MEASUREMENT
bench/godefer/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
163 lines//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(" идёт через список отложенных вызовов и не выделяет памяти.")
}