紧接前一篇 《Go1.14 为 time.Timer 定时器带来巨幅性能提升》 https://pengrl.com/p/20021/ , 本文介绍 Go1.14 针对 defer 做的优化.
在 Go1.14 之前, Go 中的每一个 defer 函数, 会在编译期在 defer 位置生成一个 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
- fmt.Println("hello")
- runtime.deferreturn() // generated for line 5
这使得使用 defer 时增加了 Go runtime 函数调用开销.
另外值得一提的是, Go runtime 中使用了先进后出的栈管理着一个函数中的多个 defer 调用, 这也意味着 defer 越多, 开销越大.
拿我们常见的互斥锁场景举例:
- var mu sync.Mutex
- mu.Lock()
- defer mu.Unlock()
defer 和非 defer 版本的 benchmark 如下:
- 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
尽管耗时都是纳秒级别, 但是 defer 版本是非 defer 版本的 2.7 倍, 换句话说, 在一些简单的锁场景, defer 的开销甚至超过了锁自身的开销. 如果在性能热点路径上, 这部分开销还是挺可观的.
这使得部分 Go 程序员在高性能编程场景下, 舍弃了 defer 的使用. 但是不使用 defer, 容易导致代码可读性下降, 资源忘记释放的问题.
于是在 Go1.14, 编译器会在某些场景下尝试在函数返回处直接调用被 defer 的函数, 从而使得使用 defer 的开销就像一个常规函数调用一样.
还拿上面那个例子举例, 编译器将生成如下的代码:
- fmt.Println("hello")
- bar() // generated for line 5
- foo() // generated for line 5
但是 defer 并不是所有场景都能内联. 比如如果是在一个循环次数可变的循环中使用 defer 就没法内联. 但是在函数起始处, 或者不包含在循环内部的条件分支中的 defer 都是可以内联的. 老实说, 我们大部分时候也就是在这些简单场景使用 defer.
在 Go1.14beta 再次执行上面 mutex 的基准测试:
- 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
可以看到 defer 版本从 26.6 ns/op 下降到了 11.5 , 与非 defer 版本的 9.64 已经非常接近了, 性能确实有大幅提升.
来源: http://www.tuicool.com/articles/aM3UfmA