内容简介:对于Go语言的defer语句,或许你回经历一个Go语言增加的比如下面的代码:
对于 Go 语言的defer语句,或许你回经历一个 赞赏 --> 怀疑 --> 肯定 --> 再怀疑 的一个过程,本文带你回顾一下defer的故事,以及如何在代码中使用defer语句。
最初的故事
Go语言增加的 defer
语句在简化代码方面确实用处多多, 尤其是对资源的释放等场景,提供了简便的代码方法。其实其它语言也有类似的语法或者语法糖, 比如 Java 就有 try-with-resource
语句,可以自动释放实现 java.io.Closeable
的对象。
比如下面的代码:
func foo(bar []string) { mu.Lock() defer mu.Unlock() if len(bar) ==0 { return } for _, s := range bar { if !strings.HasPrefix(s, "https://") { return } } ...... }
如果不使用 defer
, 代码中可能需要出现多次重复的对同一个资源的清理释放的方法调用。
func foo(bar []string) { mu.Lock() if len(bar) ==0 { mu.Unlock() return } for _, s := range bar { if !strings.HasPrefix(s, "https://") { mu.Unlock() return } } ...... mu.Unlock() }
相比较而言,第一个代码看起来比较好,锁的获取和释放成对出现,没有冗余的代码,锁的延迟释放和锁的获取紧挨着,不会忘记释放锁或者重复释放锁。
所以, 你会在很多Go的项目和库中看到 defer
的使用,而且在Go的标准库中也大量的使用(在go 1.11.2的标准库中,大约有4400多次的defer调用)。
使用defer是有代价的
随着你对Go语言的熟悉,你也许在性能测试中发现defer语句对性能的影响,也许你也阅读过一些文章, 比如雨痕的 Go 性能优化技巧 4/10 ,对defer语句带来的额外开销有一些测试。
下面是对多个defer情况的性能测试:
package test import ( "sync" "testing" ) var mu sync.Mutex //go:noinline func foo() {} //go:noinline func deferLockTwo() { mu.Lock() defer mu.Unlock() defer foo() } //go:noinline func deferLock() { mu.Lock() defer mu.Unlock() foo() } //go:noinline func deferLockClosure() { mu.Lock() defer func() { mu.Unlock() }() foo() } //go:noinline func noDeferLock() { mu.Lock() mu.Unlock() foo() } func BenchmarkDeferLockTwo(b *testing.B) { for i :=0; i < b.N; i++ { deferLockTwo() } } func BenchmarkDeferLock(b *testing.B) { for i :=0; i < b.N; i++ { deferLock() } } func BenchmarkDeferLockClosure(b *testing.B) { for i :=0; i < b.N; i++ { deferLockClosure() } } func BenchmarkNoDeferLock(b *testing.B) { for i :=0; i < b.N; i++ { noDeferLock() } }
测试结果:
BenchmarkDeferLockTwo-4 20000000 89.8 ns/op BenchmarkDeferLock-4 20000000 70.4 ns/op BenchmarkDeferLockClosure-4 20000000 67.6 ns/op BenchmarkNoDeferLock-4 100000000 19.3 ns/op
可以看到,直接的请求释放锁只需要 19.3
纳秒,可是如果通过 defer
释放锁,却需要 70.4
纳秒。
比较有意思的是,不通过 defer mu.Unlock()
,而是通过 Closure
的方式释放锁,性能会比 defer mu.Unlock()
好那么一点点。
如果代码中有多个defer, 耗费的时间更长。
可以看到,代码中使用defer, 可能会给程序的性能代码几十纳秒的开销(根据运行环境的不同,数值有所不同)。
当然, 你可以认为,几十纳秒的开销对于我的应用影响不大,一个实际的业务耗费的时间都有100毫秒,所以这个这点时间损耗不算什么。如果实际观察(比如通过pprof trace)defer语句没有影响到你的性能,那么一切还好,但是对于一个负载比较大的机器,对于 hot path
上的代码,可能需要goroutine竞争的代码,需要对性能进行进一步的优化,还是需要考虑避免对 defer
滥用。
hot paths are code execution paths in the compiler in which most of the execution time is spent, and which are potentially executed very often.
当然对于 Mutex 来说, 尽早的释放锁,在临界区结束之后, 而不是在函数返回时才释放锁是我们掌握的一个基本常识, 这样能避免无谓的过长的锁。
不在循环中使用defer也应该是我们掌握的另外一个常识, 因为循环可能产生多个defer语句,性能差,而且defer又会使资源过晚的释放。
Go编译器使用 runtime.deferproc
注册延迟调用,除了这个延迟调用的函数地址外,还会复制函数参数,在当前函数返回时,再通过 runtime.deferreturn
提取相关信息执行延迟调用, 这显然要比直接的一个函数调用指令要麻烦,也难怪性能回下降。 同时,也说明了 Closure
方式比 defer mu.Unlock()
性能要好那么一点点,因为 Closure
方式的延迟函数没有参数。
defer实现的优化和现实
Go 的代码库中也有讨论 defer
慢的issue, 在2016年曾经热烈讨论过; runtime: defer is slow
, 当时有一些项目开始注意这个问题,开始将项目中的一些defer替换成直接锁的释放, 比如 prometheus
、 x/time/rate
。
@aclements 对此进行了优化,在 Go 1.8中, defer性能提高了一倍, 当然@aclements承认defer还有优化的空间,但是目前并没有强烈的优化的意愿,除非有测试数据的支持。
@josharian也提供一个case, 他唯一一次的优化是实现一个tiny routines时候,因为涉及到了mutex, 避免过长的竞争所以避免使用 defer
。
@rhysh 也提供了一个实际的数据,他在实现一个https服务器,可以观察到 crypto/tls
和 internal/poll
的defer代码回很稳定的占用几个百分点的cpu占用, 至少,香标准库中下面的 代码
可以进行优化:
func (c *Conn) Write(b []byte) (int, error) { // interlock with Close below for { x := atomic.LoadInt32(&c.activeCall) if x&1 !=0 { return0, errClosed } if atomic.CompareAndSwapInt32(&c.activeCall, x, x+2) { defer atomic.AddInt32(&c.activeCall,-2) // 这里 break } } ......
总的来说, defer
不是免费的,但是也不是那么不堪,除非你的代码是是要频繁执行的代码,需要进行进一步的优化,可以考虑去掉defer而采用手工执行, 否则在代码中使用 defer
并不是一个问题。 从实践上,多观察pprof的监控,看看defer是不是在你的 hot path
之中。
参考资料
- https://github.com/golang/go/issues/14939
- https://medium.com/i0exception/runtime-overhead-of-using-defer-in-go-7140d5c40e32
- https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01
- https://github.com/golang/go/issues/6980
- https://github.com/golang/go/issues/20240
- https://go-review.googlesource.com/c/time/+/29379/5/rate/rate.go
- https://segmentfault.com/a/1190000005027137
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 比较 Java 枚举成员使用 equal 还是 ==
- 将CNN与RNN组合使用,天才还是错乱?
- 我应该采用 Java 12 还是坚持使用 Java 11?
- DB 分库分表(3):关于使用框架还是自主开发以及 sharding 实现层面的考量
- 如何使用JSON-simple(Java)判断返回是JSONObject还是JSONArray?
- 加班是努力还是表演?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。