Увеличивается ли емкость слайса в Go в два раза при перераспределении памяти?
Что проверяют
Знаете ли вы, что рост ёмкости не описан спецификацией и не сводится к удвоению, и понимаете ли, зачем про это вообще спрашивают. За вопросом обычно стоит практика: почему append в цикле выделяет память рывками и как этого избежать.
Как отвечать
Нет, не всегда вдвое. Удвоение работает, пока слайс маленький: примерно до нескольких сотен элементов ёмкость удваивается, дальше рост становится плавнее и приближается к коэффициенту около 1,25. Порог в актуальных версиях среды выполнения снижен, раньше он был около тысячи элементов.
Поверх этого работают ещё два правила:
- если нужной длины не хватает даже при удвоении, например при
append(s, batch...), берётся сразу требуемый размер; - итог округляется вверх до размерного класса аллокатора, поэтому фактическая ёмкость часто больше расчётной.
Проверяется это в три строки:
s := make([]int, 0)
for i := 0; i < 2000; i++ {
s = append(s, i)
fmt.Println(len(s), cap(s))
}Главный вывод для собеседования: на конкретные числа полагаться нельзя, это деталь реализации. Если размер известен заранее, задавайте его сами через make([]T, 0, n). Это убирает и перевыделения, и копирование.
Где спотыкаются
Называют коэффициент как закон языка. В спецификации сказано лишь, что ёмкости достаточно; конкретная стратегия уже менялась и может поменяться снова, поэтому уверенное «ровно вдвое» звучит хуже, чем «зависит от размера, примерно так».
Вторая ошибка: думают, что append всегда возвращает новый массив. Пока ёмкости хватает, запись идёт в тот же массив, и два слайса, полученные срезом от одного, начинают затирать данные друг друга. Отсюда берутся самые неприятные баги с общей памятью.
Третья: путают ёмкость с длиной и удивляются, что make([]int, 10) уже содержит десять нулей, а append дописывает к ним одиннадцатый элемент. Для пустого буфера с запасом нужен make([]int, 0, 10).