Где размещается большая переменная с миллионами элементов в Go
Что проверяют
Понимаете ли вы, что в Go место размещения выбирает компилятор через escape analysis, а не программист ключевыми словами. Следом обычно спрашивают, как это проверить и как большой объект влияет на сборщик мусора.
Как отвечать
Короткий ответ: почти наверняка в куче, но не из-за make или new, а по другим причинам.
- escape analysis: если значение переживает функцию (возвращается указатель, сохраняется в поле, уходит в горутину), оно размещается в куче. Иначе может остаться на стеке, даже если создано через
new; - порог размера: у компилятора есть лимит на объекты стека. Для неявных выделений (
new(T),&T{},makeс константным размером) он около 64 КБ. Миллионintэто 8 МБ, такой срез уходит в кучу, даже если никуда не убегает; makeс размером из переменной: длина неизвестна при компиляции, поэтому большой буфер в любом случае окажется в куче;- заголовок и данные: у среза три поля (указатель, длина, ёмкость). Сам заголовок может жить на стеке, а миллион элементов лежит в куче.
Проверяется это флагом go build -gcflags=-m: компилятор напишет escapes to heap или moved to heap. Гадать по коду не нужно.
Дальше про цену. Объекты больше 32 КБ рантайм выделяет напрямую из кучи, минуя классы размеров. Если в элементах нет указателей ([]int, []float64), сборщик мусора не сканирует содержимое. Миллион *Item или string сканируется целиком и заметно нагружает GC. Для повторяющихся больших буферов помогают sync.Pool и заранее выделенная ёмкость, а рост памяти ограничивают GOMEMLIMIT и GOGC.
Где спотыкаются
Говорят, что new и make всегда кладут в кучу, а литерал всегда на стек. Решает анализ, а не синтаксис.
Пугают переполнением стека от большого массива. Стек горутины растёт копированием до лимита около 1 ГБ на 64-битных системах, а переполнение обычно получают от бесконечной рекурсии.
Передают массив [1_000_000]int по значению. Массив в Go это значение, при каждом вызове копируются все 8 МБ, поэтому передают срез или указатель.