go-benchmarks · замер 001 · Go 1.27.1 · i7-6700

Указатели и сборка мусора

Десять миллионов одинаковых записей ReadState в четырёх раскладках. Данные одни и те же, меняется только число указателей. Смотрим, сколько стоит одна разметка и что происходит, когда сборку раз в две минуты запускает таймер.

Одна разметка, четыре раскладки

CPU фазы mark для кэша на 10 млн записей, медиана по семи вызовам runtime.GC(). На линейной шкале вариант без указателей не виден вовсе.

Как цена растёт с размером кэша

От 100 тыс. до 10 млн записей. У трёх вариантов с указателями цена растёт линейно с числом записей, у плоской map почти стоит на месте. Обе оси логарифмические.

Сборщик считает объекты

Те же 20 прогонов, две оси X. Плоская map на 426 MB размечается за миллисекунду, кэш на указателях того же объёма за секунду. По числу объектов в куче все четыре варианта ложатся на одну линию: около 100–200 ns CPU на объект.

по живому хипу, MB
по числу объектов в куче

Сборки по таймеру в простое

Программа заливает кэш и спит 11 минут без единой аллокации. Сборки запускает только forcegcperiod, раз в две минуты. Каждая точка: одна такая сборка, по вертикали mark по часам.

Green Tea GC

С Go 1.26 сборщик Green Tea включён по умолчанию и обходит память страницами: копит работу по спанам и сканирует объекты одного спана вместе. На этом Skylake медианы сдвигаются в обе стороны. Зато разброс между сборками с Green Tea в несколько раз меньше. Точка: медиана, линия: от минимума до максимума по семи сборкам.

Где в Go прячутся указатели

Сборщик заходит внутрь объекта, только если в его типе есть хоть одно поле-указатель. Тип без указателей помечается noscan при аллокации, и mark его не читает. Проверить можно так: если тип состоит только из чисел, bool и массивов из них, он noscan.

Что делать с большим кэшем

Способы по возрастанию усилий. Первые два закрывают большинство случаев.

  1. Ключ числом, значение структурой без указателей. map[uint64]T вместо map[string]*T. Строковый ключ превращается в хэш или id. В замере это разница между 3 s и 1,4 ms CPU на одну разметку.
  2. Значение вместо указателя. map[K]T вместо map[K]*T, []T вместо []*T. Минус один объект на запись и один указатель в слоте.
  3. Строки в один большой буфер. Байты всех строк лежат в одном []byte, в записи хранится смещение и длина (uint32). Так устроены bigcache и freecache.
  4. Списки и деревья на индексах. prev и next как int32-индексы в слайсе узлов вместо *entry. LRU остаётся LRU, сборщику обходить нечего.
  5. Крутить GOGC и GOMEMLIMIT. Реже собирает, но не дешевле: сборку по таймеру это не отменяет, и каждая всё так же обходит все объекты.
Все конфигурации: сводная таблица