当前位置: 首页 > 科技观察

使用Godefer时要小心这2个雷区!

时间:2023-03-16 16:04:05 科技观察

大家好,我是炸鱼。defer是Go语言中一个非常有趣的关键字特性。示例如下:packagemainimport"fmt"funcmain(){deferfmt.Println("friedfish")fmt.Println("braininto")}输出结果为:brainintofriedfish。小伙伴们讨论了以下问题:读者群的聊天截图。简单来说,问题就是for循环中的defer关键字会不会对性能造成影响?因为在Go语言的底层数据结构设计中,defer是一个链表数据结构:defer的基本底层结构。大家都担心如果循环太大,deferlist会很大,不够“优秀”。或者猜猜Godefer的设计是不是和Redis数据结构的设计类似。我自己优化过,但影响不大?在今天的文章中,我们将探讨循环Godefer,导致底层链表过长。它带来了什么问题,如果是,它有什么影响?开启吸鱼之路。Defer性能优化30%Go1.13早些年对defer进行了一轮性能优化,使得defer在大部分场景下性能提升了30%:Godefer1.13优化记录我们来回顾一下Go1的变化。13、看看Godefer优化在哪里,这是问题的重点。古今对比在Go1.12及之前,调用Godefer时的汇编代码如下: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.defer返回(SB)0x007f00127(main.go:7)MOVQ56(SP),Go1中的BP。13及之后,调用Godefer时汇编代码如下: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从点从程序集的角度来看,似乎最初调用了运行时。deferproc方法已更改为调用runtime.deferprocStack方法。难道是做了什么优化?我们带着疑惑继续阅读。defer的最小单位:_defer与之前的版本相比,Go中defer的最小单位是_defer结构体,主要是增加了堆字段:type_deferstruct{sizint32sizint32//includesbothargumentsandresultsstartedboolheapboolspuintptr//spattimeofdeferpcuintptrfn*funcval...这个字段用到了来识别this_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函数的函数栈指针,具体地址函数中传入的参数,以及PC(程序计数器),这块在之前的文章《深入理解 Go defer》中有详细介绍,这里不再赘述。这个deferprocStack有什么特别之处?可以看到它设置了d.heap为false,也就是说deferprocStack方法针对的是_defer在栈上分配的应用场景。deferproc的问题来了,它在哪里处理分配到堆上的应用场景?funcnewdefer(sizint32)*_defer{...d.heap=true.link=gp._defergp._defer=dreturnd}具体newdefer在Whereisitcalled中,如下:funcdeferproc(sizint32,fn*funcval){//argumentsoffnfollowfn...sp:=getcallersp()argp:=uintptr(unsafe.Pointer(&fn))+unsafe.Sizeof(fn)callerpc:=getcallerpc()d:=newdefer(siz)...}很清楚,之前版本调用的deferproc方法,现在用来对应分配到堆的场景。总结deferproc肯定没有去掉,但是优化了流程。undefined隐式循环的第二个例子就是在代码中使用goto这样的关键字:funcmain(){i:=1food:deferfunc(){}()ifi==1{i-=1gotofood}}这种写法相对很少见,因为goto关键字有时甚至被列为代码规范而没有使用,主要是因为会造成一些滥用,所以大多选择实际的方式来实现逻辑。这是一个隐式调用,会产生类似循环的效果。总结显然,Defer在设计上并不是特别精彩。主要是根据一些实际的应用场景进行优化,以获得更好的性能。defer本身虽然会带来一点开销,但是也没有想象中的那么不能用。除非你延迟的代码是需要经常执行的代码,否则你需要考虑优化它。否则,也没必要纠结太多。其实在猜测或者遇到性能问题的时候,看看PProf的分析,看看defer是不是在对应的hotpath中,然后再进行合理的优化。所谓优化,可能只是去掉defer,手动执行,并不复杂。编码时避免踩到defer的显式循环和隐式循环这两个雷区,以最大限度地提高性能。