Все уже слышали, что в Go 1.25 завезли новый экспериментальный сборщик мусора - Green Tea GC. Теории о том, как он работает, много (в том числе на Хабре).

Но когда мы с коллегой попытались просто включить GOEXPERIMENT=greenteagc на наших обычных бенчмарках, нас ждало разочарование: результаты были то чуть лучше, то чуть хуже, то вообще одинаковые. Сплошная лотерея.

Мы задались целью: найти условия, в которых Green Tea GC побеждает безоговорочно. Не на 1-2% в пределах погрешности, а так, чтобы график "пробил потолок". И у нас получилось добиться стабильного ускорения пауз GC на 40-50%.

Вот рецепт нашего успеха.

Почему не работало "в лоб"?

Обычный GC в Go (Standard) уже невероятно оптимизирован. Он отлично справляется с типичной "кашей" из разнородных объектов (строки, числа, слайсы, структуры), разбросанных по куче.

Green Tea GC - это история про векторизацию и страничное сканирование. Грубо говоря, он умеет сканировать память огромными кусками, "оптом", а не перебирать каждый объект по отдельности. Но для этого ему нужно, чтобы память была заполнена чем-то однородным и плотным.

Ингредиент 1: Pointer Storm ("Шторм указателей")

Главная фишка Green Tea GC - он невероятно быстро находит указатели. Если ваш объект содержит данные (int, string, byte), Green Tea GC не может разогнаться на полную.

Поэтому мы создали "идеального клиента" для нового GC. Структура, состоящая только из указателей, без "мусора" в виде полезной нагрузки:

// 128 байт чистых указателей.
// Это "мечта" для Green Tea GC.
type PointerBlock struct {
    P0, P1, P2, P3, P4, P5, P6, P7 *PointerBlock
    P8, P9, PA, PB, PC, PD, PE, PF *PointerBlock
}

Мы заполнили такими блоками всю доступную память, связав их в огромный граф. Для процессора это выглядит как бесконечное поле адресов, которое можно сканировать AVX-инструкциями пачками.

Ингредиент 2: Стабильная куча (Static Heap)

В реальных приложениях мы постоянно что-то создаем и удаляем. Это смешивает время аллокации (выделения памяти) и время сборки мусора.

Чтобы увидеть чистую работу GC, мы разделили тест на две фазы:

  1. Setup: Забиваем память (около 200 МБ) нашими PointerBlock под завязку.

  2. Benchmark: Перестаем мусорить! Мы просто 20 раз подряд вызываем runtime.GC().

В этом режиме GC не отвлекается на очистку памяти (удалять нечего, все объекты живые), он занимается только сканированием (Mark Phase). Именно здесь Green Tea GC должен сиять.

Ингредиент 3: Теснота (Docker Limits)

Если памяти у сервера вагон (32 ГБ), GC запускается редко и лениво. Разницу заметить трудно.
Мы загнали бенчмарк в Docker-контейнер с жестким лимитом:

docker run --memory=256m ...

Внутри мы аллоцируем ~190 МБ. Свободного места почти нет. GC вынужден работать на перделе своих возможностей.

Результаты

Запускаем наш "Pointer Storm" 10 раз подряд для каждого GC. Результаты стали стабильными, как швейцарские часы.

Метрика

Standard GC

Green Tea GC

Разница

Avg Total Time

~1025 ms

~520 ms

2x быстрее

Avg GC Pause

~51.2 ms

~26.0 ms

-49% (меньше в 2 раза!)

Вывод

Green Tea GC - это не магия, которая ускорит любой код.

  • Если у вас обычный веб-сервер, гоняющий JSON-ы туда-сюда - вы можете не заметить разницы.

  • Но если у вас огромный монолит (Gigabytes of Heap), загруженный сложными графами объектов, кэшами в памяти или деревьями - Green Tea GC может дать вам бесплатный прирост производительности в 2 раза на фазе сканирования.

А пока фича не стабилизировалась - ставьте чайник, время для экспериментов.


Статейка родилась в муках творчества, при участии Gemini 3 Pro. Вроде бы уже годный контент, как оно со стороны? Хочется прокачать навык. Фиксы можно в репку, там же лежат бенчи. Спасибо, с Новым Годом!