使用 defer 还是不使用 defer?

对于Go语言的defer语句,或许你回经历一个 赞赏 --> 怀疑 --> 肯定 --> 再怀疑的一个过程,本文带你回顾一下defer的故事,以及如何在代码中使用defer语句。

最初的故事

Go语言增加的 defer 语句在简化代码方面确实用处多多, 尤其是对资源的释放等场景,提供了简便的代码方法。其实其它语言也有类似的语法或者语法糖, 比如Java就有try-with-resource语句,可以自动释放实现java.io.Closeable的对象。

比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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, 代码中可能需要出现多次重复的对同一个资源的清理释放的方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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情况的性能测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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()
}
}

测试结果:

1
2
3
4
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.
by Konrad Rudolph

当然对于 Mutex 来说, 尽早的释放锁,在临界区结束之后, 而不是在函数返回时才释放锁是我们掌握的一个基本常识, 这样能避免无谓的过长的锁。

不在循环中使用defer也应该是我们掌握的另外一个常识, 因为循环可能产生多个defer语句,性能差,而且defer又会使资源过晚的释放。

Go编译器使用runtime.deferproc 注册延迟调用,除了这个延迟调用的函数地址外,还会复制函数参数,在当前函数返回时,再通过runtime.deferreturn提取相关信息执行延迟调用, 这显然要比直接的一个函数调用指令要麻烦,也难怪性能回下降。 同时,也说明了Closure方式比defer mu.Unlock()性能要好那么一点点,因为Closure方式的延迟函数没有参数。

defer实现的优化和现实

Go 的代码库中也有讨论defer慢的issue, 在2016年曾经热烈讨论过;runtime: defer is slow, 当时有一些项目开始注意这个问题,开始将项目中的一些defer替换成直接锁的释放, 比如prometheusx/time/rate

@aclements 对此进行了优化,在 Go 1.8中, defer性能提高了一倍, 当然@aclements承认defer还有优化的空间,但是目前并没有强烈的优化的意愿,除非有测试数据的支持。

@josharian也提供一个case, 他唯一一次的优化是实现一个tiny routines时候,因为涉及到了mutex, 避免过长的竞争所以避免使用defer

@rhysh 也提供了一个实际的数据,他在实现一个https服务器,可以观察到crypto/tlsinternal/poll的defer代码回很稳定的占用几个百分点的cpu占用, 至少,香标准库中下面的代码可以进行优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (c *Conn) Write(b []byte) (int, error) {
// interlock with Close below
for {
x := atomic.LoadInt32(&c.activeCall)
if x&1 != 0 {
return 0, errClosed
}
if atomic.CompareAndSwapInt32(&c.activeCall, x, x+2) {
defer atomic.AddInt32(&c.activeCall, -2) // 这里
break
}
}
......

总的来说, defer不是免费的,但是也不是那么不堪,除非你的代码是是要频繁执行的代码,需要进行进一步的优化,可以考虑去掉defer而采用手工执行, 否则在代码中使用defer并不是一个问题。 从实践上,多观察pprof的监控,看看defer是不是在你的hot path之中。

参考资料

  1. https://github.com/golang/go/issues/14939
  2. https://medium.com/i0exception/runtime-overhead-of-using-defer-in-go-7140d5c40e32
  3. https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01
  4. https://github.com/golang/go/issues/6980
  5. https://github.com/golang/go/issues/20240
  6. https://go-review.googlesource.com/c/time/+/29379/5/rate/rate.go
  7. https://segmentfault.com/a/1190000005027137