Go语言defer语句的三种机制整理
Golang的1.13版本与1.14版本对defer进行了两次优化,使得defer的性能开销在大部分场景下都得到大幅降低,其中到底经历了什么原理?
这是因为这两个版本对defer各加入了一项新的机制,使得defer语句在编译时,编译器会根据不同版本与情况,对每个defer选择不同的机制,以更轻量的方式运行调用。
堆上分配
在Golang1.13之前的版本中,所有defer都是在堆上分配,该机制在编译时会进行两个步骤:
- 在defer语句的位置插入runtime.deferproc,当被执行时,延迟调用会被保存为一个_defer记录,并将被延迟调用的入口地址及其参数复制保存,存入Goroutine的调用链表中。
- 在函数返回之前的位置插入runtime.deferreturn,当被执行时,会将延迟调用从Goroutine链表中取出并执行,多个延迟调用则以jmpdefer尾递归调用方式连续执行。
这种机制的主要性能问题存在于每个defer语句产生记录时的内存分配,以及记录参数和完成调用时参数移动的系统调用开销。
栈上分配
Go1.13版本新加入deferprocStack实现了在栈上分配的形式来取代deferproc,相比后者,栈上分配在函数返回后_defer便得到释放,省去了内存分配时产生的性能开销,只需适当维护_defer的链表即可。
编译器有自己的逻辑去选择使用deferproc还是deferprocStack,大部分情况下都会使用后者,性能会提升约30%。不过在defer语句出现在了循环语句里,或者无法执行更高阶的编译器优化时,亦或者同一个函数中使用了过多的defer时,依然会使用deferproc。
开放编码
Go1.14版本继续加入了开发编码(opencoded),该机制会将延迟调用直接插入函数返回之前,省去了运行时的deferproc或deferprocStack操作,在运行时的deferreturn也不会进行尾递归调用,而是直接在一个循环中遍历所有延迟函数执行。
这种机制使得defer的开销几乎可以忽略,唯一的运行时成本就是存储参与延迟调用的相关信息,不过使用此机制需要一些条件:
- 没有禁用编译器优化,即没有设置-gcflags"-N";
- 函数内defer的数量不超过8个,且返回语句与延迟语句个数的乘积不超过15;
- defer不是在循环语句中。
该机制还引入了一种元素——延迟比特(deferbit),用于运行时记录每个defer是否被执行(尤其是在条件判断分支中的defer),从而便于判断最后的延迟调用该执行哪些函数。
延迟比特的原理:
同一个函数内每出现一个defer都会为其分配1个比特,如果被执行到则设为1,否则设为0,当到达函数返回之前需要判断延迟调用时,则用掩码判断每个位置的比特,若为1则调用延迟函数,否则跳过。
为了轻量,官方将延迟比特限制为1个字节,即8个比特,这就是为什么不能超过8个defer的原因,若超过依然会选择堆栈分配,但显然大部分情况不会超过8个。
用代码演示如下:
deferBits=0//延迟比特初始值00000000 deferBits|=1<<0//执行第一个defer,设置为00000001 _f1=f1//延迟函数 _a1=a1//延迟函数的参数 ifcond{ //如果第二个defer被执行,则设置为00000011,否则依然为00000001 deferBits|=1<<1 _f2=f2 _a2=a2 } ... exit: //函数返回之前,倒序检查延迟比特,通过掩码逐位进行与运算,来判断是否调用函数 //假如deferBits为00000011,则00000011&00000010!=0,因此调用f2 //否则00000001&00000010==0,不调用f2 ifdeferBits&1<<1!=0{ deferBits&^=1<<1//移位为下次判断准备 _f2(_a2) } //同理,由于00000001&00000001!=0,调用f1 ifdeferBits&&1<<0!=0{ deferBits&^=1<<0 _f1(_a1) }
总结
以往Golangdefer语句的性能问题一直饱受诟病,最近正式发布的1.14版本终于为这个争议画上了阶段性的句号。如果不是在特殊情况下,我们不需要再计较defer的性能开销。
参考资料
[1]OuChangkun-Go语言原本
[2]峰云就她了-go1.14实现defer性能大幅度提升原理
[3]34481-opencoded-defers
到此这篇关于Go语言defer语句的三种机制整理的文章就介绍到这了,更多相关探究Go语言defer语句的三种机制内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。