MEASUREMENT
bench/goescape/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.
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
149 lines//go:build ignore
// Две задачи урока про стек, кучу и escape-анализ: обе отвечают прогоном.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Четыре функции, у каждой печатается число
// выделений в куче. Выбраны так, чтобы разрушить оба ходовых правила сразу:
// «указатель — значит куча» (случай 2: указатель есть, выделений нет) и
// «нет указателя — значит стек» (случай 4: указателя нет, выделение есть,
// потому что значение уходит в интерфейс).
//
// ВЫВОД НАМЕРЕННО БЕЗ ПОДПИСЕЙ: те же строки печатаются в русской и английской
// версиях урока, поэтому в них не должно быть слов ни одного языка.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз дороже сборка структуры,
// которая уходит в кучу, чем такой же, остающейся на стеке. Работа одинаковая
// — заполнить четыре поля; отличается только судьба результата.
//
// ЗАПУСК:
//
// go run bench/goescape/practice.go
package main
import (
"fmt"
"testing"
)
type Point struct{ X, Y, Z, W int }
// ------------------------------------------------------------- задача 1
var sinkP *Point
var sinkI any
var sinkN int
// 1. Указателя нет, значение остаётся внутри кадра.
func local() {
p := Point{1, 2, 3, 4}
sinkN = p.X + p.W
}
// 2. Указатель ЕСТЬ, но не покидает кадр: escape-анализ оставляет на стеке.
func pointerStays() {
p := &Point{1, 2, 3, 4}
sinkN = p.X + p.W
}
// 3. Указатель уходит наружу — значение обязано пережить кадр.
func pointerEscapes() {
sinkP = &Point{1, 2, 3, 4}
}
// 4. Указателя нет, но значение кладут в интерфейс: чтобы упаковать, нужен
// адрес, а он переживает кадр.
func intoInterface() {
p := Point{1, 2, 3, 4}
sinkI = p
}
func task1() {
fmt.Printf("%.0f\n", testing.AllocsPerRun(100, local))
fmt.Printf("%.0f\n", testing.AllocsPerRun(100, pointerStays))
fmt.Printf("%.0f\n", testing.AllocsPerRun(100, pointerEscapes))
fmt.Printf("%.0f\n", testing.AllocsPerRun(100, intoInterface))
}
// ------------------------------------------------------------- задача 2
// ПОЧЕМУ У ОБЕИХ ФУНКЦИЙ ЕСТЬ АРГУМЕНТ. Первая редакция строила Point из
// констант — и компилятор свернул всю арифметику в одно число ещё на сборке:
// «стековый» замер показывал 1,28 нс, то есть не измерял вообще ничего, а
// кратность выходила 35,5 из воздуха. Аргумент, известный только во время
// выполнения, свернуть нельзя, и обе функции снова делают одну и ту же работу.
//go:noinline
func makeOnStack(n int) int {
p := Point{n, n + 1, n + 2, n + 3}
return p.X + p.Y + p.Z + p.W
}
//go:noinline
func makeOnHeap(n int) *Point {
return &Point{n, n + 1, n + 2, n + 3}
}
func benchStack() float64 {
r := testing.Benchmark(func(b *testing.B) {
for i := 0; i < b.N; i++ {
sinkN = makeOnStack(i)
}
})
return float64(r.T.Nanoseconds()) / float64(r.N)
}
func benchHeap() float64 {
r := testing.Benchmark(func(b *testing.B) {
for i := 0; i < b.N; i++ {
sinkP = makeOnHeap(i)
}
})
return float64(r.T.Nanoseconds()) / float64(r.N)
}
func task2() {
// Чередующиеся круги: если машина в середине замера занялась своим,
// это ударит по обоим замерам, а не по одному.
stackBest, heapBest := benchStack(), benchHeap()
for i := 1; i < 7; i++ {
if v := benchStack(); v < stackBest {
stackBest = v
}
if v := benchHeap(); v < heapBest {
heapBest = v
}
}
ratio := heapBest / stackBest
fmt.Printf("\n структура остаётся на стеке %6.2f нс\n", stackBest)
fmt.Printf(" та же структура уходит в кучу %6.2f нс\n", heapBest)
fmt.Printf(" кратность %6.2f\n", ratio)
fmt.Printf(" она же округлённо %6.1f\n", ratio)
fmt.Printf("\n Лучший из 7 чередующихся кругов. go1.24.7 linux/amd64\n")
fmt.Printf("\n ОГОВОРКА ПРО ЗАГРУЖЕННУЮ МАШИНУ. Прежняя запись этого\n")
fmt.Printf(" прогона давала кратность 25,9 — вдвое больше нынешней.\n")
fmt.Printf(" Она была снята, пока на той же машине шла сборка сайта:\n")
fmt.Printf(" выделение в куче страдает от конкуренции за память сильнее,\n")
fmt.Printf(" чем работа со стеком, и кратность раздувается. Число,\n")
fmt.Printf(" снятое на занятой машине, измеряет машину, а не язык.\n")
fmt.Printf("\n Обе функции заполняют одну и ту же структуру из четырёх\n")
fmt.Printf(" полей. Отличается судьба результата: первая возвращает\n")
fmt.Printf(" число, вторая — указатель, и потому значение обязано\n")
fmt.Printf(" пережить кадр. Разница — цена выделения и последующей\n")
fmt.Printf(" работы сборщика, а не цена заполнения полей.\n")
fmt.Printf("\n Обе помечены //go:noinline и принимают аргумент. Без\n")
fmt.Printf(" noinline компилятор встроил бы обе в цикл и вторая\n")
fmt.Printf(" перестала бы убегать; без аргумента он свернул бы\n")
fmt.Printf(" арифметику первой в константу — и «стековый» замер\n")
fmt.Printf(" не измерял бы ничего.\n")
}
func main() {
fmt.Println("ЗАДАЧА 1 — предсказать вывод")
fmt.Println()
task1()
fmt.Println()
fmt.Println("ЗАДАЧА 2 — во сколько раз дороже уход в кучу")
task2()
}