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