Десять миллионов одинаковых записей ReadState в четырёх раскладках. Данные одни и те же, меняется только число указателей. Смотрим, сколько стоит одна разметка и что происходит, когда сборку раз в две минуты запускает таймер.
CPU фазы mark для кэша на 10 млн записей, медиана по семи вызовам runtime.GC(). На линейной шкале вариант без указателей не виден вовсе.
От 100 тыс. до 10 млн записей. У трёх вариантов с указателями цена растёт линейно с числом записей, у плоской map почти стоит на месте. Обе оси логарифмические.
Те же 20 прогонов, две оси X. Плоская map на 426 MB размечается за миллисекунду, кэш на указателях того же объёма за секунду. По числу объектов в куче все четыре варианта ложатся на одну линию: около 100–200 ns CPU на объект.
Программа заливает кэш и спит 11 минут без единой аллокации. Сборки запускает только forcegcperiod, раз в две минуты. Каждая точка: одна такая сборка, по вертикали mark по часам.
С Go 1.26 сборщик Green Tea включён по умолчанию и обходит память страницами: копит работу по спанам и сканирует объекты одного спана вместе. На этом Skylake медианы сдвигаются в обе стороны. Зато разброс между сборками с Green Tea в несколько раз меньше. Точка: медиана, линия: от минимума до максимума по семи сборкам.
Сборщик заходит внутрь объекта, только если в его типе есть хоть одно поле-указатель. Тип без указателей помечается noscan при аллокации, и mark его не читает. Проверить можно так: если тип состоит только из чисел, bool и массивов из них, он noscan.
Способы по возрастанию усилий. Первые два закрывают большинство случаев.
map[uint64]T вместо map[string]*T. Строковый ключ превращается в хэш или id. В замере это разница между 3 s и 1,4 ms CPU на одну разметку.map[K]T вместо map[K]*T, []T вместо []*T. Минус один объект на запись и один указатель в слоте.[]byte, в записи хранится смещение и длина (uint32). Так устроены bigcache и freecache.prev и next как int32-индексы в слайсе узлов вместо *entry. LRU остаётся LRU, сборщику обходить нечего.GOGC и GOMEMLIMIT. Реже собирает, но не дешевле: сборку по таймеру это не отменяет, и каждая всё так же обходит все объекты.