Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Калькулятор аллокаций Go: сколько памяти съедает ваш код

Почти каждый вопрос «почему мой сервис на Go ест память» упирается в одно и то же место: переменная, которая должна жить в стеке горутины, почему-то оказалась в куче. Причины всегда одни и те же — значение ушло в интерфейс, срез вернулся из функции, замыкание пережило вызов. Этот калькулятор показывает, сколько байт стоит каждая конструкция, где она физически лежит и что именно переносит её в кучу.

оценка для 64-битной платформыescape analysis и классы размеров

Как Go решает, где живёт значение

Go разделяет память на две большие зоны. В кадре горутины живут локальные переменные, аргументы функций и их промежуточные значения: работа с ними стоит почти ноль, потому что это обычные регистры и адреса в стеке. Размер кадра горутины стартует с пары килобайт и растёт по мере надобности. Куча — общая память процесса, откуда работает сборщик мусора: здесь живут срезы, карты, строки, интерфейсы и всё, что должно пережить вызов функции.

Решение о том, где именно окажется значение, принимает escape analysis — анализ компилятора, который строит граф «кто на кого ссылается». Если значение не переживает вызов, компилятор оставляет его в кадре. Если ссылка на него уходит наружу — в интерфейс, в возвращаемую структуру, в замыкание, — переменная переезжает в кучу, потому что иначе ссылка стала бы висячей. Никакой «магии рантайма» здесь нет: это статический анализ, и его результат видно в выводе компилятора.

Вторая механика — классы размеров. Рантайм не выделяет память «по байту»: существует лестница размеров (8, 16, 24, 32, 48, 64, 80, 96, 112, 128 байт и дальше), и любая аллокация округляется вверх до ближайшего класса. Поэтому срез из 64 элементов int занимает не 512 байт «ровно», а ближайший класс — в нашем примере 512 байт, а вот 65 элементов уже перепрыгнут на следующий класс. Эта деталь объясняет, почему профили памяти показывают круглые числа, а не те, что вы ожидали.

Ниже — интерактивная таблица. Меняйте размер среза и карты, включайте флаги «значение уходит в интерфейс», «срез возвращается наружу», «замыкание живёт дольше вызова» и смотрите, как меняются суммы на стеке и в куче. Под таблицей — разбор того, как читать вывод escape analysis, и подсказки, где искать настоящие цифры в вашем сервисе.

Что аллоцирует ваш код

Меняйте размеры и флаги — таблица и итоговые цифры пересчитаются сразу. Все значения для 64-битной платформы.

Что заставляет переменную «уехать» в кучу

Таблица: где живёт каждое значение

Сколько памяти занимает каждый конструктор Go и в какой области памяти он живёт
КонструкцияГде живётРазмерПочему так
int (обычная переменная)стек горутины8 Б на стекеПока значение не убегает, компилятор держит его в кадре горутины.
заголовок среза []intстек горутины24 Б (ptr, len, cap)Срез — это структура из трёх слов, сам массив данных лежит в куче.
массив данных среза (64 × int)куча512 БАллокация округляется до класса размера рантайма. Если размер задан константой и срез не убегает, Go выделит массив на стеке.
карта map[string]int (1000 записей)куча26.6 КБОдин бакет — 8 записей и 208 байт. Значения занимают 23.4 КБ без служебных накладных расходов.
интерфейс any / errorстек + куча16 Б (без boxing)Сам интерфейс — два указателя. Дополнительная аллокация возникает, только когда внутрь кладут значение.
замыкание с захватомстек горутины0 Б (без захвата)Замыкание без захватов компилятор вообще не создаёт.
канал с буферомкуча≈96 Б + буферСтруктура hchan выделяется в куче всегда, даже если буфер пустой.

Итог по одному вызову

На стеке
64 Б
кадр горутины, заголовки, локальные переменные
В куче
27.1 КБ
данные срезов, бакеты карты, boxing
В куче за секунду
26.50 МБ
1 000 вызовов
Стек за секунду
62.5 КБ
живёт мгновенно, но это лимит горутины

Как это выглядит в профиле

В heap-профиле вы увидите не «функцию int», а места, где аллоцируется память. Если в профиле доминируют runtime.makeslice и runtime.makemap — ищите горячий путь, который создаёт срезы или карты в цикле.

Как проверить escape analysis на своей машине

go build -gcflags='-m' ./... 2>&1 | head -40
# ./main.go:12:6: can inline add
# ./main.go:14:6: leaking param: n
# ./main.go:18:13: moved to heap: id
# ./main.go:18:22: escape analysis does not escape
Это оценка по правилам выравнивания и классов размеров, а не измерение вашего процесса. Реальные цифры отличаются: аллокации округляются до классов размеров, карта растёт с перевыделением, а сборщик мусора возвращает память не сразу. Точный ответ даёт go test -benchmem и профиль -memprofile.

Как читать escape analysis

  1. 1.does not escape — переменная остаётся в кадре горутины, аллокации нет.
  2. 2.moved to heap — компилятор перенёс переменную в кучу сам. Обычно это цена интерфейса, горутины или указателя, который ушёл за границу функции.
  3. 3.leaking param — параметр функции «утекает» наружу: функция отдаёт его ссылкой. Лечится возвратом значения, а не указателя.
  4. 4.can inline — функцию компилятор подставит по месту: дешевле, но только для простых тел.
Расчёт для GOARCH=amd64 и arm64: int — 8 байт, string — 16 байт (заголовок), срез — 24 байта (заголовок), карта — 8 байт (указатель на hmap), интерфейс — 16 байт.

Частые вопросы про память в Go

Почему мой Go-сервис ест больше памяти, чем ожидалось?

Чаще всего виноваты три вещи: срезы и карты, которые создаются в цикле или в обработчике запроса, значения, упакованные в интерфейс (fmt.Sprintf, any, ошибка), и замыкания, которые переживают вызов. Первые две группы дают аллокации на каждый запрос, третья удерживает память до сборки мусора. Откройте heap-профиль командой go tool pprof и посмотрите, какие строки dominate по аллокациям: обычно верх списка — это make в горячем пути.

Что такое escape analysis и как его включить?

Escape analysis — это статический анализ компилятора, который решает, может ли значение жить в кадре горутины или оно обязано уйти в кучу. Включается флагом -gcflags='-m': go build -gcflags='-m' ./... показывает пометки does not escape, moved to heap, leaking param. Проверить решение для конкретной функции можно через go build -gcflags='-m=2' — вывод станет подробнее.

Как уменьшить количество аллокаций в горячем пути?

Проверьте три вещи. Первое: не создавайте срезы и карты в цикле — переиспользуйте буфер или берите срез из пула. Второе: не заворачивайте числа в интерфейсы без нужды — логирование через fmt.Sprintf создаёт строку на каждый вызов, дешевле strconv.AppendInt. Третье: не держите замыкания с большими захватами — вынесите данные в структуру и передавайте её по ссылке.

Правда ли, что карта в Go медленнее среза?

Сравнивать их некорректно: у них разные задачи. Срез — это непрерывный блок данных с быстрым доступом по индексу, карта — хеш-таблица для поиска по ключу произвольного типа. Реальная разница в скорости появляется там, где вы обходите карту целиком или делаете тысячи поисков по срезу. Если нужен быстрый доступ по ключу — берите map, но помните про накладные расходы бакетов и про то, что ключ-строка хешируется при каждом обращении.

Что изучить дальше

Что внутри

Сколько стоит каждая конструкция

Калькулятор считает не абстрактные цифры, а конкретные: целое число на стеке, срез — указатель плюс три слова заголовка в куче, карта — несколько аллокаций на первую запись и рост при добавлении, интерфейс — упаковка значения в кучу вместе с потерей его типа, замыкание — отдельная окружение в куче, если оно переживает вызов.

Разница между «переменная лежит на стеке» и «переменная уехала в кучу» определяется escape analysis: компилятор смотрит, куда уходит ссылка на значение. Если ссылка не выходит за пределы функции, значение остаётся в кадре, и аллокации нет вообще. Если вы передаёте указатель в структуру, возвращаете его из функции или прячете в замыкании — значение переезжает в кучу.

Именно поэтому нельзя ответить на вопрос «сколько памяти ест мой обработчик» в отрыве от escape analysis: одна и та же переменная в одном и том же типе будет или не будет в куче в зависимости от того, как вы её используете.

Что показывает калькулятор на выходе

Для каждой конструкции выводится размер в байтах, место хранения (стек или куча), число аллокаций на операцию и причина, по которой значение ушло в кучу. Отдельно считается «память на один запрос» — если обработчик вызывается десять тысяч раз в секунду, умножение сразу показывает, где искать причину.

Для срезов и карт выводится ёмкость и длина: срез, у которого len равен нулю, а cap равен ста, удерживает весь подложенный массив — это классическая утечка, которую видно только при внимательном чтении кода. Для карт выводится оценка по числу ключей с оговоркой, что точная величина зависит от реализации хеш-таблицы.

Как пользоваться

  1. Возьмите реальную функцию, а не учебный пример. Скопируйте в калькулятор код обработчика, который у вас растёт по памяти. Учебные примеры показывают правила, а не вашу картину.
  2. Проверьте каждое место, где значение может уехать в кучу. Возврат указателя, поле структуры, замыкание, интерфейс, канал — это места, где стек превращается в кучу.
  3. Сравните с heap-профилем. Калькулятор даёт ожидание, pprof — факт. Если цифры расходятся, смотрите аллокации в горячем пути: они и создают давление на сборщик.

Когда это пригождается

Сервис ест память, а трафик не растёт

Смотрите на размер живых объектов (heap inuse), а не на сумму аллокаций (alloc space). Если inuse растёт между сборками, у вас утечка: незакрытый канал, забытая подписка, карта без вытеснения. Калькулятор покажет, какие конструкции в вашем коде чаще всего уезжают в кучу.

Профилирование показало тысячи аллокаций в обработчике

Чаще всего это fmt.Sprintf и приведение к any в цикле: каждое такое значение упаковывается в кучу. Замена на strconv.AppendInt и работа с конкретными типами убирает аллокацию целиком, а не сдвигает её.

Решили, нужен ли пул объектов

Пул имеет смысл там, где объекты большие и переиспользуются: буферы, клиенты соединений. Для коротких строк и мелких структур пул добавит блокировки и почти не сэкономит.

Размеры в калькуляторе — оценочные и опираются на типичные реализации Go. Точные числа зависят от версии рантайма, архитектуры и того, что компилятор успел доказать про ваш код. Ориентируйтесь на порядок величин и на соотношения, а не на последний байт.