Go GC corner case: GOGC=off не означает, что сборщик мусора

В Go есть настройка:

$env:GOGC = "off"

Кажется логичным, что после этого сборщик мусора перестанет запускаться. Но это не совсем так.

GOGC управляет автоматическим запуском GC на основе роста кучи.

А есть ещё одна настройка: debug.SetMemoryLimit(384 << 20)

Она задаёт мягкий лимит памяти для Go runtime.

И здесь начинается интересное: даже при GOGC=off сборщик мусора может продолжать запускаться, если runtime пытается удержать потребление памяти в пределах GOMEMLIMIT.

Решил проверить это на практике.

Условия эксперимента CPU: Intel Core i7-14700KF OS: Windows Go: 1.27.1 Live memory: 300 MiB Temporary allocations: 64 MiB × 10 Memory limit: 384 MiB

Программа удерживает 300 MiB данных, а затем десять раз выделяет по 64 MiB временной памяти.

После каждого раунда временные данные становятся недоступны для GC.

Результаты

Сначала обычный режим: GOGC=on Elapsed: 58.1144 ms GC cycles: 33 GC pause: 2.000 ms HeapAlloc after: 335 MiB HeapSys after: 435 MiB TotalAlloc: 640 MiB GC CPU fraction: 0.0132

Затем отключил автоматический триггер: GOGC=off Elapsed: 53.9909 ms GC cycles: 28 GC pause: 2.035 ms HeapAlloc after: 364 MiB HeapSys after: 391 MiB TotalAlloc: 640 MiB GC CPU fraction: 0.0236

Что получилось?

Самое интересное: GOGC=off, но GC всё равно выполнил 28 циклов.

Почему? Потому что GOGC=off отключает автоматический триггер по росту кучи, но не запрещает runtime запускать GC из-за давления на память. В нашем случае продолжал действовать лимит в 384 MiB.

Ещё один интересный момент: при GOGC=off после эксперимента HeapAlloc оказался выше, чем при обычном режиме: GOGC=on: 335 MiB GOGC=off: 364 MiB

То есть сборщик работал, но освободить всю временную память не смог: часть объектов и служебных структур ещё оставалась живой или использовалась runtime. Вывод

GOGC=off не означает «никогда не запускать GC».

А GOMEMLIMIT - это мягкий лимит, а не жёсткое ограничение памяти процесса. Поэтому при настройке Go-приложения важно учитывать обе настройки. И не стоит делать выводы о производительности по одному запуску: для этого нужны повторные benchmark-тесты.

GC можно попросить не запускаться по обычному триггеру. Но если runtime испытывает давление на память, он всё равно может вмешаться.

Ссылка на тест https://github.com/wolverineo250kr/gogc_off