Как оптимизировать память при слиянии массивов?
Что проверяют
Знаете ли вы, как устроен срез в Go (указатель на массив, длина, ёмкость) и откуда при слиянии берутся лишние аллокации. Следом почти всегда спрашивают про слияние отсортированных срезов и про ловушку с общим базовым массивом у append.
Как отвечать
- предвыделение: если итоговый размер известен, память выделяется один раз:
make([]T, 0, len(a)+len(b))и дваappend. Без этогоappendрастит ёмкость по мере заполнения: до 256 элементов удваивает, дальше увеличивает плавнее, приближаясь к 1,25 раза. Каждый рост означает новое выделение и копирование; - готовая функция: с Go 1.22 есть
slices.Concat(a, b, c), она считает суммарную длину и выделяет память один раз; - отсортированные срезы: два указателя, O(n+m) по времени, результат пишется в предвыделенный срез. Если у
aуже есть ёмкость под все элементы, слияние делается на месте, заполняя массив с конца, и дополнительная память не нужна; - повторное использование: в горячем пути буфер переиспользуется через
buf = buf[:0]илиsync.Pool, это снимает нагрузку с GC; - не материализовать: если результат нужен только для одного прохода, можно отдать итератор
iter.Seq[T](Go 1.23) и не выделять память под объединение вовсе; - измерение: бенчмарк с
b.ReportAllocs()и запуск с-benchmemпоказывают число аллокаций на операцию,pprofпоказывает, где они происходят.
Упомяните и тип элементов: срез структур хранит данные подряд, а срез указателей добавляет по объекту в куче на каждый элемент и работу для GC.
Где спотыкаются
Пишут result := append(a, b...). Если у a есть свободная ёмкость, append запишет в общий базовый массив и испортит данные тому, кто держит другой срез этого массива.
Делают make([]T, len(a)+len(b)), а потом append. Получается срез с нулями в начале и вдвое больше памяти. С ненулевой длиной нужен copy, с нулевой append.
Оставляют маленький подсрез большого массива, и сборщик не может освободить весь массив. Если нужен только кусок, скопируйте его через slices.Clone.