Deep Engineering

ЗАМЕР

bench/godefer/practice.go

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

Цитируется в статье
/ru/interview/golang/defer-panic-recover

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

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

Скрипт

163 строк
//go:build ignore

// Две задачи урока про defer, panic и recover: обе отвечают прогоном.
//
// ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ ПОД ПРАКТИКУ. Читателю в задаче говорят «вы ответили
// неверно». Если верный ответ назначен редакцией, то ошибается не читатель, а
// материал. Поэтому обе задачи урока печатаются здесь.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Три вещи, которые путают вместе: аргументы
// отложенного вызова вычисляются В МОМЕНТ defer, а не при выполнении; порядок
// выполнения обратный; и отложенная функция МОЖЕТ изменить именованный
// результат уже после return.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз дороже взять и отпустить
// мьютекс через defer, чем вручную. Здесь важно не «во сколько раз», а то, что
// сравнивается: одна и та же работа, отличается только форма освобождения.
//
// ЗАПУСК:
//
//	go run bench/godefer/practice.go
package main

import (
	"fmt"
	"runtime"
	"sync"
	"testing"
)

// ---------------------------------------------------------------- задача 1

// argsEvaluated показывает, когда вычисляются аргументы отложенного вызова.
//
// ПОЧЕМУ ПЕЧАТАЮТСЯ ГОЛЫЕ ЗНАЧЕНИЯ, БЕЗ ПОДПИСЕЙ. Вывод этой задачи читатель
// предсказывает в обеих локалях сайта, и подписи на одном языке сделали бы
// задачу непереводимой: пришлось бы либо держать две программы, либо показать
// англоязычному читателю русский вывод. Значения же одинаковы везде.
func argsEvaluated() {
	i := 0
	defer fmt.Println(i)
	i = 42
	fmt.Println(i)
}

// order показывает порядок: последний отложенный — первый выполненный.
func order() {
	for n := 1; n <= 3; n++ {
		defer fmt.Println(n)
	}
}

// namedResult возвращает 1, но отложенная функция успевает удвоить результат.
func namedResult() (result int) {
	defer func() { result *= 2 }()
	return 1
}

// anonResult возвращает то же самое, но результат НЕ именован — и удвоение
// уходит в никуда.
func anonResult() int {
	result := 1
	defer func() { result *= 2 }()
	return result
}

// recovered возвращает значение пойманной паники — и продолжает работу.
func recovered() (msg string) {
	defer func() {
		if r := recover(); r != nil {
			msg = fmt.Sprint(r)
		}
	}()
	panic("boom")
}

func task1() {
	argsEvaluated()
	order()
	fmt.Println(namedResult())
	fmt.Println(anonResult())
	fmt.Println(recovered())
}

// ---------------------------------------------------------------- задача 2

// ПОЧЕМУ СРАВНИВАЕТСЯ ИМЕННО МЬЮТЕКС. Спор про цену `defer` почти всегда идёт
// вокруг `defer mu.Unlock()`, потому что это самое частое его применение. И
// именно здесь сравнение честное: работа внутри критической секции одинаковая,
// отличается только то, как секция закрывается.
//
// Пустой `defer` в пустой функции мерить бессмысленно: с Go 1.14 открытый
// (open-coded) defer встраивается компилятором, и такой замер показал бы
// разницу двух почти пустых операций, а не то, о чём спрашивают.

var (
	mu   sync.Mutex
	data = make(map[int]int, 64)
	sink int
)

func withDefer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		func() {
			mu.Lock()
			defer mu.Unlock()
			data[i&63] = i
			sink += data[i&63]
		}()
	}
}

func manual(b *testing.B) {
	for i := 0; i < b.N; i++ {
		func() {
			mu.Lock()
			data[i&63] = i
			sink += data[i&63]
			mu.Unlock()
		}()
	}
}

const rounds = 7

func nsPerOp(r testing.BenchmarkResult) float64 {
	return float64(r.T.Nanoseconds()) / float64(r.N)
}

func main() {
	fmt.Println("ЗАДАЧА 1 — предсказать вывод")
	fmt.Println()
	task1()
	fmt.Println()

	d, m := 0.0, 0.0
	for r := 0; r < rounds; r++ {
		// Круг: оба варианта подряд, чтобы просадка машины досталась обоим.
		dd := nsPerOp(testing.Benchmark(withDefer))
		mm := nsPerOp(testing.Benchmark(manual))
		if d == 0 || dd < d {
			d = dd
		}
		if m == 0 || mm < m {
			m = mm
		}
	}

	fmt.Println("ЗАДАЧА 2 — во сколько раз дороже defer mu.Unlock()")
	fmt.Println()
	fmt.Printf("  defer mu.Unlock()           %9.2f нс\n", d)
	fmt.Printf("  mu.Unlock() вручную         %9.2f нс\n", m)
	fmt.Printf("  кратность                   %9.2f\n", d/m)
	fmt.Printf("  она же округлённо           %9.1f\n", d/m)
	fmt.Println()
	fmt.Printf("  Лучший из %d чередующихся кругов. %s %s/%s\n",
		rounds, runtime.Version(), runtime.GOOS, runtime.GOARCH)
	fmt.Println()
	fmt.Println("  Работа внутри секции одинаковая: та же запись в карту и то же")
	fmt.Println("  чтение. Отличается только форма освобождения мьютекса.")
	fmt.Println("  С Go 1.14 такой defer компилируется открытым кодом — он не")
	fmt.Println("  идёт через список отложенных вызовов и не выделяет памяти.")
}