Сборка мусора и правильное использование указателей в Go
Я пришел из Python/Ruby/JavaScript фона. Я понимаю, как работают указатели, однако я не совсем уверен, как использовать их в следующей ситуации.
Давайте представим, что у нас есть вымышленный веб-API, который выполняет поиск в некоторой базе данных изображений и возвращает JSON, описывающий то, что отображается на каждом найденном изображении:
[
{
"url": "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
"description": "Ocean islands",
"tags": [
{"name":"ocean", "rank":1},
{"name":"water", "rank":2},
{"name":"blue", "rank":3},
{"name":"forest", "rank":4}
]
},
...
{
"url": "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg",
"description": "Bridge over river",
"tags": [
{"name":"bridge", "rank":1},
{"name":"river", "rank":2},
{"name":"water", "rank":3},
{"name":"forest", "rank":4}
]
}
]
Моя цель - создать структуру данных в Go, которая будет сопоставлять каждый тег со списком URL-адресов изображений, которые будут выглядеть следующим образом:
{
"ocean": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
],
"water": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
],
"blue": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
],
"forest":[
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
],
"bridge": [
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
],
"river":[
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
]
}
Как видите, каждый URL изображения может принадлежать нескольким тегам одновременно. Если у меня есть тысячи изображений и даже больше тегов, эта структура данных может стать очень большой, если строки URL изображений копируются по значению для каждого тега. Вот где я хочу использовать указатели.
Я могу представить ответ JSON API двумя структурами в Go, func searchImages()
имитирует поддельный API:
package main
import "fmt"
type Image struct {
URL string
Description string
Tags []*Tag
}
type Tag struct {
Name string
Rank int
}
// this function mimics json.NewDecoder(resp.Body).Decode(&parsedJSON)
func searchImages() []*Image {
parsedJSON := []*Image{
&Image {
URL: "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
Description: "Ocean islands",
Tags: []*Tag{
&Tag{"ocean", 1},
&Tag{"water", 2},
&Tag{"blue", 3},
&Tag{"forest", 4},
},
},
&Image {
URL: "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg",
Description: "Bridge over river",
Tags: []*Tag{
&Tag{"bridge", 1},
&Tag{"river", 2},
&Tag{"water", 3},
&Tag{"forest", 4},
},
},
}
return parsedJSON
}
Теперь менее оптимальная функция отображения, которая приводит к очень большой структуре данных в памяти, может выглядеть следующим образом:
func main() {
result := searchImages()
tagToUrlMap := make(map[string][]string)
for _, image := range result {
for _, tag := range image.Tags {
// fmt.Println(image.URL, tag.Name)
tagToUrlMap[tag.Name] = append(tagToUrlMap[tag.Name], image.URL)
}
}
fmt.Println(tagToUrlMap)
}
Я могу изменить его, чтобы использовать указатели на Image
структура URL
поле вместо копирования по значению:
// Version 1
tagToUrlMap := make(map[string][]*string)
for _, image := range result {
for _, tag := range image.Tags {
// fmt.Println(image.URL, tag.Name)
tagToUrlMap[tag.Name] = append(tagToUrlMap[tag.Name], &image.URL)
}
}
Это работает, и мой первый вопрос: что происходит с result
структура данных после того, как я построю отображение таким образом? Будет ли Image
URL
строковые поля как-то останутся в памяти, а остальные result
будет мусор? Или будет result
структура данных остается в памяти до конца программы, потому что что-то указывает на ее участников?
Другой способ сделать это - скопировать URL-адрес в промежуточную переменную и использовать вместо него указатель:
// Version 2
tagToUrlMap := make(map[string][]*string)
for _, image := range result {
imageUrl = image.URL
for _, tag := range image.Tags {
// fmt.Println(image.URL, tag.Name)
tagToUrlMap[tag.Name] = append(tagToUrlMap[tag.Name], &imageUrl)
}
}
Это лучше? Будет ли result
структура данных быть мусором правильно?
Или, возможно, я должен использовать указатель на строку в Image
вместо структуры?
type Image struct {
URL *string
Description string
Tags []*Tag
}
Есть лучший способ сделать это? Я также был бы признателен за любые ресурсы на Go, которые подробно описывают различные способы использования указателей. Спасибо!
https://play.golang.org/p/QX36w82D-HS
ОБНОВЛЕНИЕ: я беспокоюсь об оптимальном потреблении памяти и не генерировании нежелательного мусора больше всего. Моя цель - использовать как можно меньше памяти.
2 ответа
Сначала немного предыстории. string
значения в Go представлены небольшой структурной структурой данных reflect.StringHeader
:
type StringHeader struct {
Data uintptr
Len int
}
Так что в основном передача / копирование string
значение передает / копирует это небольшое структурное значение, которое состоит из 2 слов, независимо от длины string
, На 64-битных архитектурах это всего 16 байтов, даже если string
имеет тысячу символов.
Так в основном string
ценности уже действуют как указатели. Введение другого указателя, как *string
просто усложняет использование, и вы не получите никакой заметной памяти. Ради оптимизации памяти забудьте об использовании *string
,
Это работает, и мой первый вопрос: что происходит со структурой данных результата после того, как я построю отображение таким образом? Будут ли поля памяти URL-адреса изображения каким-либо образом оставаться в памяти, а остальная часть результата будет собираться? Или структура данных результата останется в памяти до конца программы, потому что что-то указывает на ее участников?
Если у вас есть значение указателя, указывающее на поле значения структуры, тогда вся структура будет храниться в памяти, ее нельзя будет собрать мусором. Обратите внимание, что хотя можно было бы освободить память, зарезервированную для других полей структуры, но текущая среда выполнения Go и сборщик мусора этого не делают. Поэтому для достижения оптимального использования памяти вы должны забыть о хранении адресов структурных полей (если только вам не нужны полные структурные значения, но, тем не менее, хранение адресов полей и адресов элементов слайса / массива всегда требует осторожности).
Причина этого в том, что память для структурных значений выделяется как непрерывный сегмент, и поэтому сохранение только одного поля, на которое ссылаются, сильно фрагментирует доступную / свободную память и сделает оптимальное управление памятью еще сложнее и менее эффективным. Дефрагментация таких областей также потребует копирования области памяти ссылочного поля, что потребует "изменяющихся в реальном времени" значений указателя (изменение адресов памяти).
Так что при использовании указателей на string
значения могут сэкономить вам крошечную память, дополнительная сложность и дополнительные косвенные ссылки делают ее недостойной.
Так что же делать тогда?
"Оптимальным решением
Таким образом, самый чистый способ - это продолжать использовать string
ценности.
И есть еще одна оптимизация, о которой мы раньше не говорили.
Вы получаете результаты, удаляя маршалинг ответа JSON API. Это означает, что если один и тот же URL-адрес или значение тега включено в ответ JSON несколько раз, разные string
значения будут созданы для них.
Что это значит? Если в ответе JSON один и тот же URL дважды, после демаршалинга у вас будет 2 разных string
значения, которые будут содержать 2 разных указателя, указывающих на 2 разные выделенные последовательности байтов (содержимое строки, которое в противном случае будет одинаковым). encoding/json
пакет не делает string
интернирование
Вот небольшое приложение, которое доказывает это:
var s []string
err := json.Unmarshal([]byte(`["abc", "abc", "abc"]`), &s)
if err != nil {
panic(err)
}
for i := range s {
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s[i]))
fmt.Println(hdr.Data)
}
Вывод вышеизложенного (попробуйте на Go Playground):
273760312
273760315
273760320
Мы видим 3 разных указателя. Они могут быть такими же, как string
значения неизменны.
json
пакет не обнаруживает повторение string
значения, потому что обнаружение добавляет памяти и вычислительных затрат, что, очевидно, является чем-то нежелательным. Но в нашем случае мы стремимся к оптимальному использованию памяти, поэтому "начальные" дополнительные вычисления стоят большого прироста памяти.
Итак, давайте сделаем нашу собственную интернирование строк. Как это сделать?
После демаршалирования результата JSON при создании tagToUrlMap
карта, давайте отслеживать string
значения, с которыми мы столкнулись, и если последующие string
Значение было замечено ранее, просто используйте это более раннее значение (его строковый дескриптор).
Вот очень простая реализация для преобразования строк:
var cache = map[string]string{}
func interned(s string) string {
if s2, ok := cache[s]; ok {
return s2
}
// New string, store it
cache[s] = s
return s
}
Давайте проверим этот "интернер" в приведенном выше примере кода:
var s []string
err := json.Unmarshal([]byte(`["abc", "abc", "abc"]`), &s)
if err != nil {
panic(err)
}
for i := range s {
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s[i]))
fmt.Println(hdr.Data, s[i])
}
for i := range s {
s[i] = interned(s[i])
}
for i := range s {
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s[i]))
fmt.Println(hdr.Data, s[i])
}
Вывод вышеизложенного (попробуйте на Go Playground):
273760312 abc
273760315 abc
273760320 abc
273760312 abc
273760312 abc
273760312 abc
Замечательно! Как мы видим, после использования нашего interned()
функция, только один экземпляр "abc"
Строка используется в нашей структуре данных (которая на самом деле является первым появлением). Это означает, что все другие экземпляры (если их никто не использует) могут и будут надлежащим образом собираться мусором (сборщиком мусора, когда-нибудь в будущем).
Здесь не следует забывать одну вещь: строковый интернер использует cache
словарь, в котором хранятся все ранее встречающиеся строковые значения. Таким образом, чтобы эти строки пошли, вы должны "очистить" эту карту кеша, проще всего это сделать, назначив nil
значение для этого.
Без дальнейших церемоний, давайте посмотрим на наше решение:
result := searchImages()
tagToUrlMap := make(map[string][]string)
for _, image := range result {
imageURL := interned(image.URL)
for _, tag := range image.Tags {
tagName := interned(tag.Name)
tagToUrlMap[tagName] = append(tagToUrlMap[tagName], imageURL)
}
}
// Clear the interner cache:
cache = nil
Чтобы проверить результаты:
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(tagToUrlMap); err != nil {
panic(err)
}
Вывод (попробуйте на Go Playground):
{
"blue": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
],
"bridge": [
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
],
"forest": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
],
"ocean": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
],
"river": [
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
],
"water": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
]
}
Дальнейшая оптимизация памяти:
Мы использовали встроенный append()
функция для добавления новых URL изображений в теги. append()
может (и обычно делает) выделить больше срезов, чем необходимо (думая о будущем росте). После нашего процесса "сборки" мы можем пройти через tagToUrlMap
сопоставить и "обрезать" эти кусочки до необходимого минимума.
Вот как это можно сделать:
for tagName, urls := range tagToUrlMap {
if cap(urls) > len(urls) {
urls2 := make([]string, len(urls))
copy(urls2, urls)
tagToUrlMap[tagName] = urls2
}
}
Будет ли мусор правильно [...] собираться?
Да.
Вам никогда не нужно беспокоиться о том, что будет собрано что-то, что все еще используется, и вы можете положиться на все, что будет собрано, если оно больше не используется.
Таким образом, вопрос о GC никогда не будет "Будет ли он собран правильно?" но "Я генерирую ненужный мусор?". Теперь этот актуальный вопрос зависит не столько от структуры данных, сколько от количества созданных новых объектов (в куче). Так что это вопрос о том, как используются структуры данных, и гораздо меньше о самой структуре. Используйте тесты и запустите go test с -benchmem.
(Высокая производительность также может учитывать объем работы, выполняемой GC: сканирование указателей может занять время. Забудьте пока.)
Другой важный вопрос касается потребления памяти. Копирование строки копирует всего три слова, а копирование строки * копирует одно слово. Так что здесь не так много безопасных, используя * string.
Так что, к сожалению, нет четких ответов на соответствующие вопросы (количество сгенерированного мусора и общее потребление памяти). Не задумывайтесь над проблемой, используйте то, что соответствует вашим целям, оцените и проведите рефакторинг.