Deep Engineering

ЗАМЕР

bench/goescape/practice.go

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/golang/escape-analysis

Запись прогона

У этого замера записи прогона нет — только скрипт.

Скрипт

149 строк
//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()
}