golang defer性能损耗和实际使用场景

golang defer性能损耗和实际使用场景

我们常常听到别人说:”defer 在栈退出时执行,会有性能损耗,尽量不要用。“ 前面的博客 defer原理 我们分析了defer延迟调用的底层实现原理 。下面我们就基于那篇原理分析文章,来分析一下 defer 延迟调用的性能损耗。

基准测试

package main

import (
	"sync"
	"testing"
)

var lock sync.Mutex

func NoDefer() {
	lock.Lock()
	lock.Unlock()
}
func Defer() {
	lock.Lock()
	defer lock.Unlock()
}

func BenchmarkNoDefer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		NoDefer()
	}
}

func BenchmarkDefer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Defer()
	}
}

benchmark的执行结果如下:

ytlou@ytlou-mac  ~/Desktop/golang/golang_study/study/basic/defer   master ●✚  go test -bench=. defer_latency_test.go
goos: darwin
goarch: amd64
BenchmarkNoDefer-12     90020019                11.3 ns/op
BenchmarkDefer-12       37018846                33.0 ns/op
PASS
ok      command-line-arguments  2.294s

我们19款16G内存Mac上测试,结果显示使用defer后的函数开销确实比没使用高了不少,这损耗用到哪里去了呢?

defer开销

我们回忆一下 defer原理 中对于defer延迟调用编译成汇编之后的汇编代码,defer 关键字其实涉及了一系列的连锁调用,内部 runtime 函数的调用就至少多了三步,分别是 runtime.deferproc 一次和 runtime.deferreturn 两次。

而这还只是在运行时的显式动作,另外编译器做的事也不少,例如:

  • 在 deferprocStack或者deferproc 阶段(注册延迟调用),还得获取/传入目标函数地址、函数参数等等。
  • 在 deferreturn 阶段,需要在函数调用结尾处插入该方法的调用,同时若有被 defer 的函数,还需要使用 runtime·jmpdefer 进行跳转以便于后续调用。

这一些动作途中还要涉及最小单元 _defer 的获取/创建(在堆或栈), defer 和 recover 链表的逻辑处理和消耗等动作。

实际场景

defer 很多使用场景都是资源的close, 确保资源能够在函数栈调用结束的时候释放资源。比如下面的例子:

resp, err := http.Get(...)
if err != nil {
    return err
}
defer resp.Body.Close()

但是一定得这么写吗?其实并不,很多人给出的理由都是 “怕你忘记” 这种说辞,这没有毛病。但需要认清场景,假设我的应用场景如下:

resp, err := http.Get(...)
if err != nil {
    return err
}
defer resp.Body.Close()
// do something
time.Sleep(time.Second * 60)

嗯,一个请求当然没问题,流量、并发一下子大了呢,那可能就是个灾难了。你想想为什么?并发高的时候,持有成千上万的 http response对象,但是又没有及时的释放掉,而是使用 defer延迟释放,导致并发高的时候造成很严重的性能问题。

从常见的 defer + close 的使用组合来讲,用之前建议先看清楚应用场景,在保证无异常的情况下确保尽早关闭才是首选。如果只是小范围调用很快就返回的话,偷个懒直接一套组合拳出去也未尝不可。

结论

一个 defer 关键字实际上包含了不少的动作和处理,和你单纯调用一个函数一条指令是没法比的。而与对照物相比,它确确实实是有性能损耗,目前延迟调用的全部开销大约在 30ns,但 defer 所提供的作用远远大于此,你从全局来看,它的损耗非常小,并且官方还不断地在优化中。

因此,对于 “Go defer 会有性能损耗,尽量不能用?” 这个问题,我认为该用就用,应该及时关闭就不要延迟,使用时一定要想清楚场景。

还有一点:defer使用最重要的不是性能问题,defer 最大的功能是 Panic 后依然有效。如果没有 defer,Panic 后就会导致 unlock 丢失,从而导致死锁了。 我觉得这个case非常经典。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章