MEASUREMENT
bench/gogc/practice.go
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/golang/gc
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
This measurement has no recorded run — only the script.
Script
159 lines//go:build ignore
// Две задачи урока про сборщик мусора: обе отвечают прогоном.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Пять чисел про одну арифметику: GOGC задаёт,
// НАСКОЛЬКО куче позволено вырасти сверх живых данных, прежде чем начнётся
// следующий цикл. Отсюда и 2 при GOGC=100, и 5 при GOGC=400, и ноль циклов
// при выключенном сборщике.
//
// ПОЧЕМУ ЭТО НЕ «СКОЛЬКО ПАМЯТИ РАЗРЕШЕНО»: GOGC — не предел, а множитель к
// текущему объёму живых данных. Вырастут живые — вырастет и цель, никакого
// потолка в этом числе нет. Потолок задаётся отдельно, через GOMEMLIMIT.
//
// ВЫВОД НАМЕРЕННО БЕЗ ПОДПИСЕЙ: те же строки печатаются в русской и английской
// версиях урока, поэтому в них не должно быть слов ни одного языка.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз меньше циклов сборки на одном и
// том же объёме выделений, если поднять GOGC со 100 до 400.
//
// ЗАПУСК:
//
// go run bench/gogc/practice.go
package main
import (
"fmt"
"runtime"
"runtime/debug"
)
var (
live [][]byte
sink []byte
)
// Набрать заданный объём ЖИВЫХ данных: они остаются достижимыми через live,
// поэтому сборщик их не тронет и цель следующего цикла считается от них.
func fillLive(mb int) {
live = nil
runtime.GC()
for i := 0; i < mb; i++ {
live = append(live, make([]byte, 1<<20))
}
runtime.GC()
}
func round(v float64) int { return int(v + 0.5) }
// Во сколько раз цель следующего цикла больше живых данных.
func goalOverLive(pct int) float64 {
debug.SetGCPercent(pct)
fillLive(16)
var m runtime.MemStats
runtime.ReadMemStats(&m)
return float64(m.NextGC) / float64(m.HeapAlloc)
}
// Сколько циклов сборки случится, пока выделяется фиксированный объём мусора
// ПРИ ФИКСИРОВАННОМ ОБЪЁМЕ ЖИВЫХ ДАННЫХ.
//
// ПОЧЕМУ ЖИВЫЕ ДАННЫЕ ЗДЕСЬ ОБЯЗАТЕЛЬНЫ. Первая редакция замера обнуляла
// live перед циклом — и получала кратность то 3,6, то 8,5 от прогона к
// прогону. Причина не в шуме машины: без живых данных цель следующего цикла
// упирается в нижнюю границу кучи (4 МБ), а не считается от GOGC, и число
// циклов перестаёт зависеть от того, что замер меряет. С фиксированными
// 16 МБ живых данных цель считается арифметикой GOGC, и кратность выходит
// той, которую эта арифметика и предсказывает.
func cyclesFor(pct int) uint32 {
fillLive(16)
debug.SetGCPercent(pct)
runtime.GC()
var a runtime.MemStats
runtime.ReadMemStats(&a)
for i := 0; i < 200000; i++ {
sink = make([]byte, 1024)
}
var b runtime.MemStats
runtime.ReadMemStats(&b)
return b.NumGC - a.NumGC
}
// Медиана: одиночный выброс не должен решать, что напечатано в уроке.
func medianCycles(pct int, rounds int) uint32 {
got := make([]uint32, 0, rounds)
for i := 0; i < rounds; i++ {
got = append(got, cyclesFor(pct))
}
for i := 1; i < len(got); i++ {
for j := i; j > 0 && got[j] < got[j-1]; j-- {
got[j], got[j-1] = got[j-1], got[j]
}
}
return got[len(got)/2]
}
// ---------------------------------------------------------------- задача 1
func task1() {
// SetGCPercent возвращает ПРЕДЫДУЩЕЕ значение — отсюда идиома
// «поменять на время и вернуть как было».
fmt.Println(debug.SetGCPercent(100))
// Цель следующего цикла = живые данные × (1 + GOGC/100).
// Округление вынесено в helper, а не в Printf: эти строки целиком
// переносятся в блок предсказания урока, а обратный слэш в шаблонной
// строке MDX означал бы там перевод строки, а не два символа.
fmt.Println(round(goalOverLive(100)))
fmt.Println(round(goalOverLive(400)))
// С выключенным сборщиком циклов нет вовсе, сколько ни выделяй.
fmt.Println(cyclesFor(-1))
// А ручной вызов считается циклом, даже когда сборщик выключен.
debug.SetGCPercent(-1)
var a runtime.MemStats
runtime.ReadMemStats(&a)
runtime.GC()
runtime.GC()
var b runtime.MemStats
runtime.ReadMemStats(&b)
fmt.Println(b.NumGC - a.NumGC)
debug.SetGCPercent(100)
}
// ---------------------------------------------------------------- задача 2
func task2() {
lo := medianCycles(100, 5)
hi := medianCycles(400, 5)
debug.SetGCPercent(100)
ratio := float64(lo) / float64(hi)
fmt.Printf("\n живых данных всё время %6d МБ\n", 16)
fmt.Printf(" циклов сборки при GOGC=100 %6d\n", lo)
fmt.Printf(" циклов сборки при GOGC=400 %6d\n", hi)
fmt.Printf(" кратность %6.2f\n", ratio)
fmt.Printf(" она же округлённо %6.1f\n", ratio)
fmt.Printf("\n Медиана 5 кругов. go1.24.7 linux/amd64\n")
fmt.Printf("\n Выделено в обоих случаях одинаково: 200 000 кусков по\n")
fmt.Printf(" килобайту при одних и тех же 16 МБ живых данных.\n")
fmt.Printf(" Отличается только порог: при GOGC=100 цикл начинается,\n")
fmt.Printf(" когда куча дорастает до 32 МБ, при GOGC=400 — до 80 МБ.\n")
fmt.Printf(" То есть за цикл успевает накопиться вчетверо больше\n")
fmt.Printf(" мусора, и циклов нужно во столько же раз меньше.\n")
fmt.Printf("\n Но это не бесплатное ускорение, а обмен: реже циклы —\n")
fmt.Printf(" выше пик памяти. Ручка GOGC не делает сборку дешевле,\n")
fmt.Printf(" она переносит цену со времени на память.\n")
}
func main() {
fmt.Println("ЗАДАЧА 1 — предсказать вывод")
fmt.Println()
task1()
fmt.Println()
fmt.Println("ЗАДАЧА 2 — во сколько раз меньше циклов при GOGC=400")
task2()
}