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