Kodazm

Инлайнинг defer

Перевод “Inlined defers in Go”.

defer в Go позволяет запланировать вызов функции перед выходом из основной функции. Это не обязательно должна быть одна функция - можно запланировать вызов нескольких функций. Как правило, defer используется для очистки ресурсов, завершения задач и тд. Такие запланированные функции хорошо использовать для обслуживания. Например, с помощью defer мы точно не забудем закрыть файл.

func main() {
    f, err := os.Open("hello.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    // The rest of the program...
}

Defer позволяет откладывать вызов метода f.Close(), а запланировать вызов этот метод можно как только появится необходимый контекст. Использование такой конструкции повышает читемость кода.

Как работает defer

defer обрабатывает несколько функций, собирая их последовательно в стек и запуская по очереди в порядке LIFO(последний пришел - первый ушел). Чем больше функций запланировано, тем больше будет стек.

func main() {
	for i := 0; i < 5; i++ {
		defer fmt.Printf("%v ", i)
	}
}

Пример выше распечатает в консоль 4 3 2 1 0, потому что последняя отложенная функция выполняется в первую очередь.

Когда функция откладывается, переменные, к которым она обращается, сохраняются как ее аргументы. Для каждой отложенной функции компилятор генерирует вызов runtime.deferproc в момент определения defer и вызов в runtime.deferreturn в точке выхода из функции.

func run() {
    defer foo()
    defer bar()

    fmt.Println("hello")
}

Для кода выше компилятор сгенерирует:

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 и без

var mu sync.Mutex
mu.Lock()

defer 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(в некоторых случаях) не будет отличаться от обычного вызова функций.

func run() {
    defer foo()
    defer bar()

    fmt.Println("hello")
}

С новыми улучшениями, код выше будет инлайнится в такой:

// Other code...

bar() // generated for line 5
foo() // 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.

comments powered by Disqus