Память в Go
Размещение на стеке и в куче, escape-анализ, принудительная куча, new против make и локальность кэша.
8 вопросов
JuniorТеорияОчень частоВ чём разница между размещением в стеке и в куче в Go?
В чём разница между размещением в стеке и в куче в Go?
Каждая goroutine имеет свой небольшой растущий стек для короткоживущих локальных переменных; он освобождается автоматически при возврате из кадра. Куча хранит значения, переживающие свой кадр, и освобождается сборщиком мусора. Где живёт значение, решает компилятор, а не программист.
Типичные ошибки
- ✗Считать, что место (стек/куча) определяет ключевое слово (
var,new,make), а не escape analysis компилятора - ✗Думать, что стек goroutine имеет фиксированный размер — он начинается маленьким и растёт по мере надобности
- ✗Полагать, что указательные типы всегда живут в куче
Уточняющие вопросы
- →Как runtime увеличивает стек goroutine, когда место заканчивается?
- →Почему размещение в куче дороже размещения в стеке в Go?
JuniorТеорияЧастоКак посмотреть решения компилятора по escape-анализу в Go?
Как посмотреть решения компилятора по escape-анализу в Go?
Передайте -gcflags=-m в go build или go run: go build -gcflags=-m ./... печатает каждое решение, например moved to heap: x или &x escapes to heap. Повторите флаг (-gcflags='-m -m') для обоснования решения. Изменений в коде не требуется.
Типичные ошибки
- ✗Искать runtime-флаг, тогда как escape-анализ — это решение времени компиляции, печатаемое компилятором
- ✗Путать отчёт аллокаций heap-профайлера с решениями компилятора по каждой переменной
- ✗Забыть
./...или путь к пакету, из-за чего флаг ни к чему не применяется
Уточняющие вопросы
- →Что добавляет второй
-m(-gcflags='-m -m') в вывод? - →Почему переменная, которую вы ждали на стеке, показывает
escapes to heap?
JuniorТеорияЧастоВ чём разница между встроенными new и make в Go?
В чём разница между встроенными new и make в Go?
new(T) выделяет обнулённую память под значение любого типа T и возвращает указатель *T на нулевое значение. make работает только для слайсов, map и каналов: он инициализирует внутреннюю структуру и возвращает готовое значение типа T, а не указатель.
Типичные ошибки
- ✗Думать, что
new([]int)даёт пригодный слайс — он возвращает*[]int, указывающий на nil-слайс; нуженmake([]int, 0) - ✗Считать, что
makeвозвращает указатель, а не само инициализированное значение типа T - ✗Считать, что место (стек/куча) определяет ключевое слово, а не escape analysis компилятора
Уточняющие вопросы
- →Почему
new(map[string]int)нельзя использовать для записи? - →Что на самом деле решает, окажется ли значение, выделенное через
new, в стеке или в куче?
MiddleТеорияЧастоЧто такое escape analysis и как компилятор решает между стеком и кучей?
Что такое escape analysis и как компилятор решает между стеком и кучей?
Escape analysis — это анализ времени компиляции, решающий, где живёт значение. Если компилятор доказывает, что время жизни значения не выходит за кадр, оно попадает в стек. Если время жизни может пережить кадр — адрес возвращён, сохранён в объекте кучи или захвачен убегающим замыканием — оно убегает в кучу.
Типичные ошибки
- ✗Называть escape analysis runtime-механизмом — это статический анализ времени компиляции
- ✗Считать, что взятие адреса локальной переменной всегда вызывает размещение в куче — она убегает, только если адрес переживает кадр
- ✗Думать, что escape решает сам тип (указатель или значение), а не способ использования значения
Уточняющие вопросы
- →Как
go build -gcflags='-m'показывает решение об escape для значения? - →Почему возврат указателя на локальную переменную безопасен в Go, но является UB в C?
MiddleПроизводительностьИногдаПочему при последовательном обходе непрерывный slice обычно быстрее связного списка?
Почему при последовательном обходе непрерывный slice обычно быстрее связного списка?
Slice хранит элементы непрерывно, поэтому обход идёт по одному дружественному кэшу блоку памяти — префетчер CPU подгружает следующие элементы заранее, и промахи кэша редки (пространственная локальность). Связный список разбрасывает узлы по heap, поэтому каждый next — это переход по указателю на непредсказуемый адрес, что вызывает частые промахи кэша и простои.
Типичные ошибки
- ✗Рассуждать только об big-O, игнорируя константные эффекты кэша
- ✗Думать, что переходы по указателям в списке так же дружественны кэшу, как непрерывный проход
- ✗Списывать разницу на GC или проверки границ, а не на раскладку памяти
Уточняющие вопросы
- →Что такое строка кэша, и как она объясняет преимущество префетчера на slice?
- →Когда связный список всё же будет правильным выбором, несмотря на стоимость обхода?
MiddleТеорияИногдаКогда функции в Go стоит возвращать значение, а когда указатель?
Когда функции в Go стоит возвращать значение, а когда указатель?
Маленькие простые структуры возвращайте по значению: копия дешёвая, значение может остаться на стеке и не создаёт нагрузки на GC. Указатель возвращайте, когда структура большая (копирование дорого), когда вызывающие должны делить и менять один экземпляр, или когда nil — осмысленный результат. Указатель часто вызывает escape в heap, и значение добавляет работы сборщику мусора.
Типичные ошибки
- ✗Рефлекторно везде возвращать указатели, добавляя лишние escape в heap и работу GC
- ✗Думать, что возврат по значению только для примитивов, а не для маленьких структур
- ✗Игнорировать, что возврат указателя на локальную переменную обычно вызывает аллокацию в heap
Уточняющие вопросы
- →Как
go build -gcflags=-mпокажет, что возвращаемое значение убежало в heap? - →Почему возврат
nilкак sentinel-значения склоняет API к возврату указателя?
SeniorТеорияИногдаПомимо обычного escape, какие случаи принудительно размещают значение в куче в Go?
Помимо обычного escape, какие случаи принудительно размещают значение в куче в Go?
Помимо обычного escape, компилятор размещает значение в куче, если размер неизвестен на этапе компиляции — make с переменной длиной или append, растящий backing array — если значение слишком велико для стека, и если оно убегает через преобразование к interface или reflection.
Типичные ошибки
- ✗Думать, что escape analysis — единственное, что вообще помещает значение в кучу
- ✗Считать, что
makeс переменной во время выполнения длиной всё ещё может разместиться в стеке - ✗Полагать, что значение никогда не убегает в кучу при преобразовании к interface
Уточняющие вопросы
- →Почему преобразование маленького значения к
interface{}часто вызывает размещение в куче? - →Как компилятор выбирает порог размера стека, выше которого значение обязано убежать?
SeniorТеорияРедкоДействительно ли чтение значения из стека быстрее, чем из кучи, в Go?
Действительно ли чтение значения из стека быстрее, чем из кучи, в Go?
Не само чтение: как только адрес в регистре, загрузка стоит одинаково независимо от того, где лежит значение — процессор не отличает стек от кучи. Стековое размещение выигрывает в другом: нет сканирования и сборки GC, почти бесплатные выделение/освобождение сдвигом указателя стека, и лучшая локальность кеша из-за горячих смежных фреймов. Выигрыш — про аллокацию и нагрузку на GC, а не про задержку чтения.
Типичные ошибки
- ✗Считать, что процессор читает стековую память более быстрыми инструкциями, чем память кучи
- ✗Приписывать скорость стека задержке чтения, а не стоимости аллокации и нагрузке на GC
- ✗Думать, что чтение из кучи платит блокировку или барьер сборщика мусора на каждом доступе
Уточняющие вопросы
- →Почему побег в кучу вредит пропускной способности, даже если каждый отдельный доступ так же быстр?
- →Как локальность кеша смежных стековых фреймов влияет на реальную производительность чтения?