「译」 Go1.14将内联defer提高性能

紧接前一篇《Go1.14 为 time.Timer 定时器带来巨幅性能提升》[1],本文介绍 Go1.14 针对 defer 做的优化。

在 Go1.14 之前,Go 中的每一个 defer 函数,会在编译期在 defer 位置生成一个runtime.deferproc调用,并且在包含 defer 的函数退出时生成一个runtime.deferreturn调用。

如下代码:

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

编译器会生成类似如下的代码:

<code>runtime.deferproc(foo) // generated for line 1
runtime.deferproc(bar) // generated for line 2

fmt.Println("hello")

runtime.deferreturn() // generated for line 5
/<code>

这使得使用 defer 时增加了 Go runtime 函数调用开销。

另外值得一提的是,Go runtime 中使用了先进后出的栈管理着一个函数中的多个 defer 调用,这也意味着 defer 越多,开销越大。

拿我们常见的互斥锁场景举例:

<code>var mu sync.Mutex
mu.Lock()

defer mu.Unlock()
/<code>

defer 和非 defer 版本的 benchmark 如下:

<code>BenchmarkMutexNotDeferred-8 \t125341258    9.55 ns/op\t       0 B/op\t       0 allocs/op
BenchmarkMutexDeferred-8 \t45980846 26.6 ns/op\t 0 B/op\t 0 allocs/op
/<code>

尽管耗时都是纳秒级别,但是 defer 版本是非 defer 版本的 2.7 倍,换句话说,在一些简单的锁场景,defer 的开销甚至超过了锁自身的开销。如果在性能热点路径上,这部分开销还是挺可观的。

这使得部分 Go 程序员在高性能编程场景下,舍弃了 defer 的使用。但是不使用 defer,容易导致代码可读性下降,资源忘记释放的问题。

于是在 Go1.14,编译器会在某些场景下尝试在函数返回处直接调用被 defer 的函数,从而使得使用 defer 的开销就像一个常规函数调用一样。

还拿上面那个例子举例,编译器将生成如下的代码:

<code>fmt.Println("hello")

bar() // generated for line 5
foo() // generated for line 5
/<code>

但是 defer 并不是所有场景都能内联。比如如果是在一个循环次数可变的循环中使用 defer 就没法内联。但是在函数起始处,或者不包含在循环内部的条件分支中的 defer 都是可以内联的。老实说,我们大部分时候也就是在这些简单场景使用 defer。

在 Go1.14beta 再次执行上面 mutex 的基准测试:

<code>BenchmarkMutexNotDeferred-8 \t123710856    9.64 ns/op\t       0 B/op\t       0 allocs/op
BenchmarkMutexDeferred-8 \t104815354 11.5 ns/op\t 0 B/op\t 0 allocs/op
/<code>

可以看到 defer 版本从26.6 ns/op下降到了11.5,与非 defer 版本的9.64已经非常接近了,性能确实有大幅提升。

本文内容来自一篇英文文章,但是省去了原文中关于 defer 的基础功能介绍,只对 defer 内联部分的内容进行意译,没有逐字翻译,感兴趣的可以观看原文: https://rakyll.org/inlined-defers/

Go 内联 defe 的设计、提案:https://github.com/golang/proposal/blob/master/design/34481-opencoded-defers.md

[1]

《Go1.14 为 time.Timer 定时器带来巨幅性能提升》: https://pengrl.com/p/20021/


「译」 Go1.14将内联defer提高性能


分享到:


相關文章: