原文地址:Go defer 会有性能损耗,尽可能不要用?html
上个月在 @polaris @轩脉刃 的全栈技术群里看到一个小伙伴问 “说 defer 在栈退出时执行,会有性能损耗,尽可能不要用,这个怎么解?”。git
刚好前段时间写了一篇 《深刻理解 Go defer》 去详细剖析 defer
关键字。那么这一次简单结合前文对这个问题进行探讨一波,但愿对你有所帮助,但在此以前但愿你花几分钟,本身思考一下答案,再继续往下看。github
func DoDefer(key, value string) { defer func(key, value string) { _ = key + value }(key, value) } func DoNotDefer(key, value string) { _ = key + value }
基准测试:golang
func BenchmarkDoDefer(b *testing.B) { for i := 0; i < b.N; i++ { DoDefer("煎鱼", "https://github.com/EDDYCJY/blog") } } func BenchmarkDoNotDefer(b *testing.B) { for i := 0; i < b.N; i++ { DoNotDefer("煎鱼", "https://github.com/EDDYCJY/blog") } }
输出结果:segmentfault
$ go test -bench=. -benchmem -run=none goos: darwin goarch: amd64 pkg: github.com/EDDYCJY/awesomeDefer BenchmarkDoDefer-4 20000000 91.4 ns/op 48 B/op 1 allocs/op BenchmarkDoNotDefer-4 30000000 41.6 ns/op 48 B/op 1 allocs/op PASS ok github.com/EDDYCJY/awesomeDefer 3.234s
从结果上来,使用 defer
后的函数开销确实比没使用高了很多,这损耗用到哪里去了呢?并发
$ go tool compile -S main.go "".main STEXT size=163 args=0x0 locals=0x40 ... 0x0059 00089 (main.go:6) MOVQ AX, 16(SP) 0x005e 00094 (main.go:6) MOVQ $1, 24(SP) 0x0067 00103 (main.go:6) MOVQ $1, 32(SP) 0x0070 00112 (main.go:6) CALL runtime.deferproc(SB) 0x0075 00117 (main.go:6) TESTL AX, AX 0x0077 00119 (main.go:6) JNE 137 0x0079 00121 (main.go:7) XCHGL AX, AX 0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB) 0x007f 00127 (main.go:7) MOVQ 56(SP), BP 0x0084 00132 (main.go:7) ADDQ $64, SP 0x0088 00136 (main.go:7) RET 0x0089 00137 (main.go:6) XCHGL AX, AX 0x008a 00138 (main.go:6) CALL runtime.deferreturn(SB) 0x008f 00143 (main.go:6) MOVQ 56(SP), BP 0x0094 00148 (main.go:6) ADDQ $64, SP 0x0098 00152 (main.go:6) RET ...
咱们在前文提到 defer
关键字其实涉及了一系列的连锁调用,内部 runtime
函数的调用就至少多了三步,分别是 runtime.deferproc
一次和 runtime.deferreturn
两次。函数
而这还只是在运行时的显式动做,另外编译器作的事也很多,例如:性能
deferproc
阶段(注册延迟调用),还得获取/传入目标函数地址、函数参数等等。deferreturn
阶段,须要在函数调用结尾处插入该方法的调用,同时如有被 defer
的函数,还须要使用 runtime·jmpdefer
进行跳转以便于后续调用。这一些动做途中还要涉及最小单元 _defer
的获取/生成, defer
和 recover
链表的逻辑处理和消耗等动做。测试
最后讨论的时候有提到 “问题指的是原本就是用来执行 close() 一些操做的,而后说尽可能不能用,例子就把 defer db.close() 前面的 defer 删去了” 这个疑问。优化
这是一个比较相似 “教科书” 式的说法,在一些入门教程中会潜移默化的告诉你在资源控制后加个 defer
延迟关闭一下。例如:
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)
嗯,一个请求固然没问题,流量、并发一会儿大了呢,那可能就是个灾难了。你想一想为何?从常见的 defer
+ close
的使用组合来说,用以前建议先看清楚应用场景,在保证无异常的状况下确保尽早关闭才是首选。若是只是小范围调用很快就返回的话,偷个懒直接一套组合拳出去也何尝不可。
一个 defer
关键字实际上包含了很多的动做和处理,和你单纯调用一个函数一条指令是无法比的。而与对照物相比,它确确实实是有性能损耗,目前延迟调用的所有开销大约在 50ns,但 defer
所提供的做用远远大于此,你从全局来看,它的损耗很是小,而且官方还不断地在优化中。
所以,对于 “Go defer 会有性能损耗,尽可能不能用?” 这个问题,我认为该用就用,应该及时关闭就不要延迟,在 hot paths 用时必定要想清楚场景。
补充上柴大的回复:“不是性能问题,defer 最大的功能是 Panic 后依然有效。若是没有 defer,Panic 后就会致使 unlock 丢失,从而致使死锁了”,很是经典。