воскресенье, 7 июня 2020 г.

Встроенные отсрочки в Golang

Ключевое слово defer в Go позволяет нам запланировать запуск функций до возврата родительской функции. Несколько функций могут быть отложены (deferred) в родительской функции. defer часто используется для очистки ресурсов, завершения задач в области функций и т. п. Отложенные функции отлично подходят для удобства обслуживания. Откладывая, например, мы уменьшаем риск забыть закрыть файл в остальной части программы:

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

    // Остальная часть программы...
}

Отсрочка помогает нам, задерживая выполнение метода Close, позволяя нам печатать его, когда мы имеем правильный контекст. Именно так отложенные функции также помогают удобочитаемости исходного кода.

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

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

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

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

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

0: func run() {
1:    defer foo()
2:    defer bar()
3:
4:    fmt.Println("hello")
5: }

Компилятор сгенерирует код, подобный приведенному ниже, для программы выше:

runtime.deferproc(foo) // генерируется для строки 1
runtime.deferproc(bar) // генерируется для строки 2

// Другой код...

runtime.deferreturn() // генерируется для строки 5

Производительность defer

defer раньше требовала двух дорогих вызовов времени выполнения, описанных выше. Это делало отложенные функции значительно более дорогими, чем неотложенные функции. Например, рассмотрим блокировку и разблокировку sync.Mutex с отложенным и не отложенным использованием.

var mu sync.Mutex
mu.Lock()

defer mu.Unlock()

Программа выше будет работать в 1,7 раза медленнее, чем не отложенная версия. Несмотря на то, что для блокировки и разблокировки мьютекса с помощью отсрочки требуется всего ~25-30 наносекунд, это имеет значение при широкомасштабном использовании или в случаях, когда вызов функции должен быть завершен менее чем за 10 наносекунд.

BenchmarkMutexNotDeferred-8    125341258          9.55 ns/op        0 B/op        0 allocs/op
BenchmarkMutexDeferred-8       45980846         26.6 ns/op        0 B/op        0 allocs/op

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

Встраивание отложенных функций

В последних нескольких версиях Go были постепенные улучшения производительности defer. Но с Go 1.14 в некоторых распространенных случаях значительно улучшится производительность. Компилятор сгенерирует код для встраивания некоторых отложенных функций в точках возврата. С этим улучшением вызов некоторых отложенных функций будет стоить столько же, сколько и обычный вызов функции.

0: func run() {
1:    defer foo()
2:    defer bar()
3:
4:    fmt.Println("hello")
5: }

С новыми улучшениями приведенный выше код сгенерирует:

// Другой код...

bar() // генерируется для строки 5
foo() // генерируется для строки 5

Это улучшение можно сделать только в статических случаях. Например, в цикле, где выполнение динамически определяется размером ввода, компилятор не имеет возможности сгенерировать код для встраивания всех отложенных функций. Но в простых случаях (например, откладывание наверху функции или в условных блоках, если они не находятся в циклах), можно встроить отложенные функции. Начиная с версии 1.14, простые случаи будут встроены, а координация во время выполнения потребуется только в том случае, если компилятор не может сгенерировать код.

Вот пример выполнения на 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 - это хорошая версия для переоценки отсрочки, если вы избегаете defer для увеличения производительности.


Читайте также:


Комментариев нет:

Отправить комментарий