В своем докладе на GolangConf 2023 я буду рассказывать про эволюцию Go, в том числе и про новые пакеты в стандартной библиотеке. Изучая один из этих пакетов мне стало интересно, как там реализована функция
Delete. А реализация там крайне простая:func Delete[S ~[]E, E any](s S, i, j int) S {
_ = s[i:j] // bounds check
return append(s[:i], s[j:]...)
}
И вроде бы все верно, так? Даже те из вас, кто давно работают с Go скорее всего не смогут сходу сказать, что тут не так. Признаюсь, я к пониманию глубины проблемы пришел только в рамках подготовки самих слайдов.
Допустим у нас есть такой код:
var slc []*int64
for i := 0; i < 10_000_000; i++ {
val := i
slc = append(slc, &val)
}
slc = slices.Delete(slc, 1, 10_000_000)
runtime.GC()
// Report memory metrics
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc = %v MiB\n", m.Alloc / 1024 / 1024)
Первичный анализ подсказывает, что после вызова GC у нас должен остаться в памяти слайс на 10 миллионов указателей и на один int. Ну т.е. программа в памяти будет занимать от 80 до 90 МБ на amd64.
Пробуем запустить и получаем вывод:
Alloc = 170 MiB
Наш анализ некорректен: в Go сборщик мусора не собирает память на которую есть указатели в активной памяти, даже если эти указатели находятся за пределами длинны слайса. Причина простая: никто не мешает вам сделать от слайса выше новый слайс с длинной 3 и тем самым восстановить указатель на 3ий элемент.
Интересно то, что этой проблемой озаботились не в Go Core Team, а простой Go разработчик
DeleteFunc, Compact, CompactFunc и Replace. При этом сам issue есть эталон того как это правильно делать: там и лаконичная мысль, разбитая на параграфы, и картинки которые круто визуализируют проблему, и разные варианты решений. В общем понравилось не только мне - Расс тоже оценил. А решение выбрал максимально простое: будем занулять элементы которые выкинули, благо у нас теперь есть встроенная функция
clear. Текущий статус: likely accept. А это значит, что пока есть все шансы, что мы увидим сие в 1.22.