Что происходит при выделении одного килобайта памяти в Go
Что проверяют
Знаете ли вы, что «выделить память» в Go это две разные истории, стек и куча, и что выбирает между ними компилятор, а не вы. Дальше смотрят, представляете ли вы аллокатор и сборщик мусора хотя бы схемой, без чтения исходников рантайма.
Как отвечать
Начните с развилки. Компилятор проводит анализ убегания: если значение не покидает функцию, оно ложится на стек горутины, это бесплатно и освобождается само на выходе. Стек горутины стартует маленьким и растёт копированием, поэтому килобайт на нём не проблема. Решение компилятора видно через go build -gcflags=-m.
Если значение убегает, например уходит в возвращаемое значение, в интерфейс или в замыкание, оно идёт в кучу, и дальше работает свой аллокатор, не системный:
- килобайт это мелкий объект, для него есть готовый класс размеров;
- блок берётся из локального кэша, привязанного к процессору, без блокировок; кончился запас, идут в общий центральный список и затем в кучу;
- у ядра память просят большими областями и заранее, а не по куску на каждый объект;
- объекты без указателей лежат в отдельных спанах, сборщик их вообще не сканирует.
Сборщик конкурентный, помечает и метёт, объекты не перемещает, запускается не по таймеру, а по росту кучи относительно живых данных, плюс есть мягкий лимит по памяти.
Где спотыкаются
Говорят, что Go зовёт системный malloc. Системный аллокатор тут не участвует: рантайм сам берёт у ядра крупные области и нарезает их по классам размеров, поэтому обычное выделение до системного вызова не доходит.
Уверены, что куча всегда медленная и надо любой ценой оставлять всё на стеке. Дорого не само выделение, а последующая работа сборщика, поэтому оптимизируют не одну аллокацию, а их количество на горячем пути, и начинают с профиля, а не с догадок.
Ждут, что после сборки память тут же вернётся системе. Освобождённые страницы отдаются лениво, так что занятая процессом память какое-то время остаётся высокой, и судить по ней о размере живых данных нельзя.