Инлайнинг 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.