当前位置: 首页 > 后端技术 > PHP

Go1.13defer的性能提升如何?

时间:2023-03-29 20:47:35 PHP

Go1.13最近终于发布了。其中一个值得注意的特点是,defer在大多数场景下都提升了30%的性能,但是官方并没有具体说明如何提升,这让大家很是疑惑。又因为之前写过《深入理解 Go defer》、《Go defer 会有性能损耗,尽量不要用?》这样的文章,所以对它到底做了哪些改变才得到这个结果很感兴趣,所以今天就和大家一起探秘。原文地址:Go1.13defer性能提升如何?1.测试Go1.12$gotest-bench=.-benchmem-run=nonegoos:darwingoarch:amd64pkg:github.com/EDDYCJY/awesomeDeferBenchmarkDoDefer-42000000091.4ns/op48B/op1allocs/opBenchmarkDoNotDefer-430041.0000ns/op48B/op1allocs/opPASSokgithub.com/EDDYCJY/awesomeDefer3.234sGo1.13$去测试-bench=。-benchmem-run=nonegoos:darwingoarch:amd64pkg:github.com/EDDYCJY/awesomeDeferBenchmarkDoDefer-41598606274.7ns/op48B/op1allocs/opBenchmarkDoNotDefer-42923184240.3ns/op48B/op1allocs/opPASSokcom/EDDYCJY/awesomeDefer3.444s在测试用例中,确实在这两个版本中,defer的性能都有所提升,但似乎并没有100%提升30%。2.看之前(Go1.12)0x007000112(main.go:6)CALLruntime.deferproc(SB)0x007500117(main.go:6)TESTLAX,AX0x007700119(main.go:6)JNE1370x007900121(main.go:7)XCHGLAX,AX0x007a00122(main.go:7)CALLruntime.deferreturn(SB)0x007f00127(main.go:7)MOVQ56(SP),BPnow(Go1.13)0x006e00110(main.go:4)MOVQAX,(SP)0x007200114(main.go:4)CALLruntime.deferprocStack(SB)0x007700119(main.go:4)TESTLAX,AX0x007900121(main.go:4)JNE1390x007b00123(main.go:7)XCHGLAX,AX0x007c00124(main.go:7)CALLruntime.deferreturn(SB)0x008100129(main.go:7)MOVQ112(SP),BP从汇编的角度看,好像改成了runtime.deferproc调用runtime.deferprocStack。难道是做了什么优化?我们带着疑惑继续阅读。3.观察源码_defertype_deferstruct{sizint32sizint32//包括参数和结果startedboolheapboolspuintptr//defer时的sppcuintptrfn*funcval...和之前的版本相比,最小unit_defer结构体主要是增加了一个heap字段,用来标识这个_defer是分配在堆上还是分配在栈上。其余的字段没有明显变化,所以我们可以关注defer的堆栈分配来,看看做了什么。deferprocStackfuncdeferprocStack(d*_defer){gp:=getg()ifgp.m.curg!=gp{throw("deferonsystemstack")}d.started=falsed.heap=falsed.sp=getcallersp()d.pc=getcallerpc()*(*uintptr)(unsafe.Pointer(&d._panic))=0*(*uintptr)(unsafe.Pointer(&d.link))=uintptr(unsafe.Pointer(gp._defer)))*(*uintptr)(unsafe.Pointer(&gp._defer))=uintptr(unsafe.Pointer(d))return0()}这段代码比较常规,主要是获取调用defer的函数栈指针function,pass输入函数的参数和PC(程序计数器)的具体地址,在之前的文章《深入理解 Go defer》中已经详细介绍过,这里不再赘述。那么这个deferprocStack有什么特别之处呢?我们可以看到它设置了d.heap为false,也就是说deferprocStack方法针对的是_defer分配在栈上的应用场景。deferproc那么问题来了,它在哪里处理分配到堆上的应用场景呢?funcnewdefer(sizint32)*_defer{...d.heap=trued.link=gp._defergp._defer=dreturnd}那么newdefer在哪里调用,如下:funcdeferproc(sizint32,fn*funcval){//fn的参数跟在fn...sp:=getcallersp()argp:=uintptr(unsafe.Pointer(&fn))+unsafe.Sizeof(fn)callerpc:=getcallerpc()d:=newdefer(siz)...}就很清楚了,之前版本调用的deferproc方法,现在用来对应分配到堆上的场景。第一点小结:可以肯定的是,deferproc并没有被去掉,只是流程进行了优化。第二点:编译器会根据应用场景选择使用deferproc还是deferprocStack方法。它们分别针对分配在堆上和栈上的使用场景。4.编译器如何选择esc//src/cmd/compile/internal/gc/esc.gocaseODEFER:ife.loopdepth==1{//topleveln.Esc=EscNever//强制堆栈分配defer记录(见ssa.go)break}ssa//src/cmd/compile/internal/gc/ssa.gocaseODEFER:d:=callDeferifn.Esc==EscNever{d=callDeferStack}s.call(n.Left,d)总结从这个组合来看,核心是当e.loopdepth==1时,逃逸分析结果n.Esc会被设置为EscNever,也就是分配_defer入栈。那么这个e.loopdepth在哪里呢?天哪,我们详细看一下代码,如下://src/cmd/compile/internal/gc/esc.gotypeNodeEscStatestruct{Curfn*NodeFlowsrc[]EscStepRetvalNodesLoopdepthint32LevelLevelWalkgenuint32Maxextraloopdepthint32}的keyhere查看Loopdepth字段,目前有3个值标识,分别是:-1:全局。0:返回变量。1:顶层函数,或内部函数的递增值。这样读起来有点迷惑,结合我们上面e.loopdepth==1的表达方式,即deferfunc是顶层函数时,会分配到栈上。但是,如果在deferfunc之外有一个显式迭代循环,或者如果有一个隐式迭代,它将分配在堆上。实际上,深度代表迭代的深度。我们可以确认刚才提到的方向。显式迭代的代码如下:funcmain(){forp:=0;p<10;p++{deferfunc(){因为我:=0;我<20;i++{log.Println("EDDYCJY")}}()}}检查编译:$gotoolcompile-Smain.go"".mainSTEXTsize=122args=0x0locals=0x200x000000000(main.go:15)TEXT"".main(SB),ABIInternal,$32-0...0x004800072(main.go:17)CALLruntime.deferproc(SB)0x004d00077(main.go:17)TESTLAX,AX0x004f00079(main.go:17)JNE830x005100081(main.go:17)JMP330x005300083(main.go:17)XCHGLAX,AX0x005400084(main.go:17)CALLruntime.defer返回(SB)...很明显,最后的defer调用了runtime.deferproc方法,也就是在堆上分配了,没有错。对于隐式迭代,可以使用goto语句来实现这个功能,然后自己验证,这里不再赘述。总结从分析结果来看,Go1.13官方的defer性能提升了30%,主要是其延迟对象的栈分配规则发生了变化。措施是通过编译器分析defer的for循环迭代深度。如果loopdepth为1,设置逃逸分析结果分配到栈上,否则分配到堆上。确实,我个人认为对于大部分的使用场景,它都做了很多优化,也解决了一些人吐槽的“差”的defer性能问题。另外,我觉得从Go1.13开始,你也需要稍微了解一下它的机制,不要随便搞个野版的nestediterationdefer,不一定能把性能发挥到极致。如果想了解更多细节,可以看看defer的提交,官方的测试用例也包含在里面。