Инлайнинг defer

12 minute read

Перевод “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.