Инлайнинг defer

Перевод “Inlined defers in Go”.
defer в Go позволяет запланировать вызов функции перед выходом из основной функции. Это не обязательно должна быть одна функция - можно запланировать вызов нескольких функций. Как правило, defer используется для очистки ресурсов, завершения задач и тд. Такие запланированные функции хорошо использовать для обслуживания. Например, с помощью defer мы точно не забудем закрыть файл.
1func main() {
2    f, err := os.Open("hello.txt")
3    if err != nil {
4        log.Fatal(err)
5    }
6    defer f.Close()
7
8    // The rest of the program...
9}
Defer позволяет откладывать вызов метода f.Close(), а запланировать вызов этот метод можно как только появится необходимый контекст. Использование такой конструкции повышает читемость кода.
Как работает defer
defer обрабатывает несколько функций, собирая их последовательно в стек и запуская по очереди в порядке LIFO(последний пришел - первый ушел). Чем больше функций запланировано, тем больше будет стек.
1func main() {
2	for i := 0; i < 5; i++ {
3		defer fmt.Printf("%v ", i)
4	}
5}
Пример выше распечатает в консоль 4 3 2 1 0, потому что последняя отложенная функция выполняется в первую очередь.
Когда функция откладывается, переменные, к которым она обращается, сохраняются как ее аргументы. Для каждой отложенной функции компилятор генерирует вызов runtime.deferproc в момент определения defer и вызов в runtime.deferreturn в точке выхода из функции.
1func run() {
2    defer foo()
3    defer bar()
4
5    fmt.Println("hello")
6}
Для кода выше компилятор сгенерирует:
runtime.deferproc(foo) // generated for line 1
runtime.deferproc(bar) // generated for line 2
// Other code...
runtime.deferreturn(bar) // generated for line 5
runtime.deferreturn(foo) // generated for line 5
Производительность defer
defer подразумевает два довольно дорогих системных вызова. Поэтому вызов отложенной функции значительно дороже, чем вызов обычной функции. Например, сравните вызов блокировки/разблокировки sync.Mutex в вариантах с defer и без
1var mu sync.Mutex
2mu.Lock()
3
4defer mu.Unlock()
Программа выше работаем в 1.7 раз дольше, чем программа без defer. Даже если учитывать, что блокировка/разблокировка мьютекса занимает ~20-30 наносекунд, это имеет значение на больших объемах или когда вызов функции должен сработать за определенное время.
BenchmarkMutexNotDeferred-8   	125341258	         9.55 ns/op	       0 B/op	       0 allocs/op
BenchmarkMutexDeferred-8      	45980846	        26.6 ns/op	 
Из-за этих накладных расходов разработчики начали избегать использования defer для повышения производительности. К сожалению, такой подход ухудшает читаемость кода.
Инлайнинг запланированных функций
В последних версиях Go добавили много улучшений производительности defer. В Go 1.14, в некоторых случаях, будет значительное улучшение производительности defer. Компилятор будет генерировать код, в котором запланированные вызовы будут инлайниться в точке возврата. С таким подходом, использование defer(в некоторых случаях) не будет отличаться от обычного вызова функций.
1func run() {
2    defer foo()
3    defer bar()
4
5    fmt.Println("hello")
6}
С новыми улучшениями, код выше будет инлайнится в такой:
1// Other code...
2
3bar() // generated for line 5
4foo() // generated for line 5
Такое улучшение возможно только для статичных случаев. А, например, для циклов так не получится сделать, потому что кол-во определений defer может динамически изменятся в зависимости от логики программы и компилятор не сможет сгенерировать инлайновый код. Но для простых случаев(как в примере с блокировками выше) будет работать инлайнинг. С версии 1.14 все простые случае с defer будут инланиться и работать быстро, как без defer.
Если прогнать бенчмарки на версии Go 1.14beta, то код с отложенными вызовами и без работает примерно одинаково:
BenchmarkMutexNotDeferred-8   	123710856	         9.64 ns/op	       0 B/op	       0 allocs/op
BenchmarkMutexDeferred-8      	104815354	        11.5 ns/op	       0 B/op	       0 allocs/op
Go 1.14 это отличное время, чтобы переосмыслить свой подход к defers. Если вам интересно узнать больше про это улучшение - пройдитесь по ссылкам Low-cost defers through inline code proposal и GoTime’s recent episode on defer with Dan Scales.