MEASUREMENT
bench/godefer/internals.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
300 lines//go:build ignore
// defer, panic и recover изнутри: три вида defer, цена паники и то, чего
// recover не ловит.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Про `defer` ходят два противоположных суеверия — «он
// дорогой, не используйте в горячем коде» и «он бесплатный, с Go 1.14». Оба
// неверны, потому что `defer` компилируется тремя разными способами, и цена
// отличается на порядок. Различить их на глаз нельзя: в исходнике это одно и
// то же слово.
//
// Про `recover` суеверие одно и опаснее: «он ловит паники». Ловит он ровно
// одну — ту, что летит в ТЕКУЩЕЙ горутине через ТЕКУЩИЙ отложенный вызов.
// Всё остальное проходит мимо, и проверяется это только запуском.
//
// ЗАПУСК:
//
// go run bench/godefer/internals.go
package main
import (
"errors"
"fmt"
"runtime"
"testing"
)
// ------------------------------------------------------- блок 1: три вида
var sink int
// openCoded — фиксированное число defer в функции без циклов. С Go 1.14
// компилятор разворачивает такой defer в обычный код на выходе из функции:
// ни списка, ни выделения памяти.
func openCoded() {
defer func() { sink++ }()
sink++
}
// inLoop — defer внутри цикла. Число отложенных вызовов заранее неизвестно,
// поэтому открытый код неприменим: каждый вызов приходится РЕГИСТРИРОВАТЬ в
// списке отложенных вызовов горутины.
//
// ВАЖНО ПРО ПАМЯТЬ. Первая редакция этого файла утверждала, что запись при
// этом попадает в кучу. Замер ниже показал ноль выделений, и утверждение
// пришлось убрать: запись кладётся на стек (`deferprocStack`). Дорожает не
// выделение, а сама регистрация и последующий обход списка.
func inLoop(n int) {
for i := 0; i < n; i++ {
defer func() { sink++ }()
}
sink++
}
// conditional — defer под условием: тоже открытый код, пока функция без цикла.
func conditional(flag bool) {
if flag {
defer func() { sink++ }()
}
sink++
}
func kinds() {
fmt.Println("ТРИ ВИДА DEFER: ОДНО СЛОВО, РАЗНАЯ ЦЕНА")
fmt.Println("───────────────────────────────────────")
a1 := testing.AllocsPerRun(1000, func() { openCoded() })
a2 := testing.AllocsPerRun(1000, func() { conditional(true) })
a3 := testing.AllocsPerRun(1000, func() { inLoop(8) })
fmt.Printf(" один defer в функции выделений: %.0f\n", a1)
fmt.Printf(" defer под условием выделений: %.0f\n", a2)
fmt.Printf(" defer в цикле, 8 витков выделений: %.0f\n", a3)
fmt.Println()
one := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
openCoded()
}
})
loop8 := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
inLoop(8)
}
})
loop64 := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
inLoop(64)
}
})
fmt.Printf(" один defer в функции %8.2f нс\n", one)
fmt.Printf(" defer в цикле, 8 витков %8.2f нс (%.1f нс на виток)\n", loop8, loop8/8)
fmt.Printf(" defer в цикле, 64 витка %8.2f нс (%.1f нс на виток)\n", loop64, loop64/64)
fmt.Println()
fmt.Println(" Выделений в куче нет НИ В ОДНОМ случае — это стоит сказать")
fmt.Println(" прямо, потому что «defer в цикле выделяет память» повторяют")
fmt.Println(" часто. Запись об отложенном вызове кладётся на стек.")
fmt.Println()
fmt.Println(" Первая строка по времени — тот самый «бесплатный» defer:")
fmt.Println(" компилятор разворачивает его в обычный код на выходе из")
fmt.Println(" функции. Третья — тот самый «дорогой»: число отложенных")
fmt.Println(" вызовов заранее неизвестно, открытый код неприменим, и")
fmt.Println(" каждый вызов приходится регистрировать в списке.")
fmt.Println()
fmt.Println(" И главное про цикл — не цена, а СРОК. Отложенный вызов")
fmt.Println(" выполняется на выходе из ФУНКЦИИ, а не из витка: файлы,")
fmt.Println(" открытые в цикле, останутся открытыми до конца функции.")
fmt.Println()
}
// ------------------------------------------------- блок 2: чего не ловит
// directRecover — единственная работающая форма: recover вызван
// НЕПОСРЕДСТВЕННО в отложенной функции.
func directRecover() (caught bool) {
defer func() {
if recover() != nil {
caught = true
}
}()
panic("раз")
}
// helperRecover — recover вызван из вложенной функции. Он вернёт nil, и
// паника продолжит лететь: перехватывать её приходится вторым уровнем.
func helperRecover() (caughtInside, caughtOutside bool) {
defer func() {
if recover() != nil {
caughtOutside = true
}
}()
defer func() {
// recover() внутри вложенного вызова не работает.
caughtInside = helper()
}()
panic("два")
}
func helper() bool { return recover() != nil }
// afterRecover показывает, что функция НЕ продолжается с места паники:
// выполняются только отложенные вызовы, а тело дальше не идёт.
func afterRecover() (reached bool) {
defer func() { recover() }()
panic("три")
//nolint:govet // недостижимость здесь и есть предмет наблюдения
}
// rePanic: паника, поднятая заново, летит дальше с новым значением.
func rePanic() (msg string) {
defer func() {
if r := recover(); r != nil {
msg = fmt.Sprint(r)
}
}()
func() {
defer func() {
if r := recover(); r != nil {
panic(fmt.Sprint("обёрнуто: ", r))
}
}()
panic("исходная")
}()
return
}
// nilPanic: panic(nil) с Go 1.21 превращается в *runtime.PanicNilError.
func nilPanic() (kind string) {
defer func() {
r := recover()
kind = fmt.Sprintf("%T", r)
}()
panic(nil)
}
func limits() {
fmt.Println("ЧТО RECOVER ЛОВИТ, А ЧТО НЕТ")
fmt.Println("────────────────────────────")
fmt.Printf(" recover прямо в отложенной функции поймал: %v\n", directRecover())
in, out := helperRecover()
fmt.Printf(" recover из вложенного вызова поймал: %v\n", in)
fmt.Printf(" та же паника, пойманная уровнем выше поймал: %v\n", out)
fmt.Printf(" тело функции после паники выполнилось: %v\n", afterRecover())
fmt.Printf(" паника, поднятая заново %q\n", rePanic())
fmt.Printf(" panic(nil) с Go 1.21 даёт тип %s\n", nilPanic())
fmt.Println()
fmt.Println(" Первые две строки — самое частое место ошибки: recover")
fmt.Println(" работает, только когда вызван НЕПОСРЕДСТВЕННО в отложенной")
fmt.Println(" функции. Обёртка в помощник «для чистоты» его отключает")
fmt.Println(" молча — ни ошибки сборки, ни предупреждения.")
fmt.Println()
fmt.Println(" Чего здесь нет и быть не может: паники ДРУГОЙ горутины и")
fmt.Println(" fatal error рантайма. Первую нельзя поймать снаружи — у")
fmt.Println(" каждой горутины свой стек отложенных вызовов; вторая не")
fmt.Println(" паника вовсе. Проверить это программой нельзя: оба случая")
fmt.Println(" завершают процесс, что и есть ответ.")
fmt.Println()
}
// ---------------------------------------------- блок 3: паника против ошибки
var errFail = errors.New("не вышло")
func returnsError(fail bool) error {
if fail {
return errFail
}
return nil
}
func panicsAndRecovers(fail bool) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
}
}()
if fail {
panic(errFail)
}
return nil
}
func costOfPanic() {
fmt.Println("ПАНИКА ПРОТИВ ВОЗВРАТА ОШИБКИ, НА ОДИНАКОВОЙ РАБОТЕ")
fmt.Println("───────────────────────────────────────────────────")
okErr := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
if returnsError(false) != nil {
sink++
}
}
})
failErr := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
if returnsError(true) != nil {
sink++
}
}
})
okPanic := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
if panicsAndRecovers(false) != nil {
sink++
}
}
})
failPanic := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
if panicsAndRecovers(true) != nil {
sink++
}
}
})
fmt.Printf(" return nil %10.2f нс\n", okErr)
fmt.Printf(" return err %10.2f нс\n", failErr)
fmt.Printf(" defer+recover, паники нет %10.2f нс\n", okPanic)
fmt.Printf(" defer+recover, паника есть %10.2f нс\n", failPanic)
fmt.Printf(" паника против возврата %10.2f\n", failPanic/failErr)
fmt.Println()
fmt.Println(" Третья строка важнее четвёртой: даже когда паники НЕ")
fmt.Println(" случилось, форма с defer+recover стоит дороже обычного")
fmt.Println(" возврата — цену платит каждый вызов, а не только сбойный.")
fmt.Println()
fmt.Println(" Отсюда правило, а не запрет: паника — для того, что не")
fmt.Println(" должно случаться (нарушен инвариант, невозможное состояние).")
fmt.Println(" Ожидаемый сбой — это ошибка в возврате, и разница между ними")
fmt.Println(" не в стиле, а в том, кто обязан её обработать.")
fmt.Println()
}
// ------------------------------------------------------------ утилиты
const rounds = 7
func nsPerOp(r testing.BenchmarkResult) float64 {
return float64(r.T.Nanoseconds()) / float64(r.N)
}
func best(f func(*testing.B)) float64 {
out := 0.0
for r := 0; r < rounds; r++ {
v := nsPerOp(testing.Benchmark(f))
if out == 0 || v < out {
out = v
}
}
return out
}
func main() {
fmt.Printf("%s %s/%s | defer, panic и recover изнутри\n\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH)
kinds()
limits()
costOfPanic()
}