探究 Go 语言 defer 语句的三种机制
Golang 的 1.13 版本 与 1.14 版本对 defer 进行了两次优化,使得 defer 的性能开销在大部分场景下都得到大幅降低,其中到底经历了什么原理?
这是因为这两个版本对 defer
各加入了一项新的机制,使得 defer
语句在编译时,编译器会根据不同版本与情况,对每个 defer
选择不同的机制,以更轻量的方式运行调用。
堆上分配
在 Golang 1.13 之前的版本中,所有 defer
都是在堆上分配,该机制在编译时会进行两个步骤:
- 在
defer
语句的位置插入runtime.deferproc
,当被执行时,延迟调用会被保存为一个_defer
记录,并将被延迟调用的入口地址及其参数复制保存,存入 Goroutine 的调用链表中。 - 在函数返回之前的位置插入
runtime.deferreturn
,当被执行时,会将延迟调用从 Goroutine 链表中取出并执行,多个延迟调用则以 jmpdefer 尾递归调用方式连续执行。
这种机制的主要性能问题存在于每个 defer
语句产生记录时的内存分配,以及记录参数和完成调用时参数移动的系统调用开销。
栈上分配
Go 1.13 版本新加入 deferprocStack
实现了在栈上分配的形式来取代 deferproc
,相比后者,栈上分配在函数返回后 _defer
便得到释放,省去了内存分配时产生的性能开销,只需适当维护 _defer
的链表即可。
编译器有自己的逻辑去选择使用 deferproc
还是 deferprocStack
,大部分情况下都会使用后者,性能会提升约 30%。不过在 defer
语句出现在了循环语句里,或者无法执行更高阶的编译器优化时,亦或者同一个函数中使用了过多的 defer
时,依然会使用 deferproc
。
开放编码
Go 1.14 版本继续加入了开发编码(open coded),该机制会将延迟调用直接插入函数返回之前,省去了运行时的 deferproc
或 deferprocStack
操作,在运行时的 deferreturn
也不会进行尾递归调用,而是直接在一个循环中遍历所有延迟函数执行。
这种机制使得 defer
的开销几乎可以忽略,唯一的运行时成本就是存储参与延迟调用的相关信息,不过使用此机制需要一些条件:
- 没有禁用编译器优化,即没有设置
-gcflags "-N"
; - 函数内
defer
的数量不超过 8 个,且返回语句与延迟语句个数的乘积不超过 15; defer
不是在循环语句中。
该机制还引入了一种元素 —— 延迟比特(defer bit),用于运行时记录每个 defer
是否被执行(尤其是在条件判断分支中的 defer
),从而便于判断最后的延迟调用该执行哪些函数。
延迟比特的原理: 同一个函数内每出现一个 defer
都会为其分配 1 个比特,如果被执行到则设为 1,否则设为 0,当到达函数返回之前需要判断延迟调用时,则用掩码判断每个位置的比特,若为 1 则调用延迟函数,否则跳过。
为了轻量,官方将延迟比特限制为 1 个字节,即 8 个比特,这就是为什么不能超过 8 个 defer
的原因,若超过依然会选择堆栈分配,但显然大部分情况不会超过 8 个。
用代码演示如下:
deferBits = 0 // 延迟比特初始值 00000000 deferBits |= 1<<0 // 执行第一个 defer,设置为 00000001 _f1 = f1 // 延迟函数 _a1 = a1 // 延迟函数的参数 if cond { // 如果第二个 defer 被执行,则设置为 00000011,否则依然为 00000001 deferBits |= 1<<1 _f2 = f2 _a2 = a2 } ... exit: // 函数返回之前,倒序检查延迟比特,通过掩码逐位进行与运算,来判断是否调用函数 // 假如 deferBits 为 00000011,则 00000011 & 00000010 != 0,因此调用 f2 // 否则 00000001 & 00000010 == 0,不调用 f2 if deferBits & 1<<1 != 0 { deferBits &^= 1<<1 // 移位为下次判断准备 _f2(_a2) } // 同理,由于 00000001 & 00000001 != 0,调用 f1 if deferBits && 1<<0 != 0 { deferBits &^= 1<<0 _f1(_a1) }
总结
以往 Golang defer 语句的性能问题一直饱受诟病,最近正式发布的 1.14 版本终于为这个争议画上了阶段性的句号。如果不是在特殊情况下,我们不需要再计较 defer 的性能开销。
参考资料
[1] Ou Changkun - Go 语言原本:
https://changkun.de/golang/zh-cn/part2runtime/ch09lang/defer/
[2] 峰云就她了 - go1.14实现defer性能大幅度提升原理:
http://xiaorui.cc/archives/6579
[3] 34481-opencoded-defers:
https://github.com/golang/proposal/blob/master/design/34481-opencoded-defers.md
本文属于原创,首发于微信公众号「面向人生编程」,如需转载请后台留言。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
两个桶兑出特定容积的水
面试的时候,可能会经常碰到这样一个问题:嘉定区有两个桶,一个容量为 3 升,一个容量为 5 升,我们怎么能够不通过其他度量工具的帮助兑出 1 升的水来。假定水是无限的。 !! 此处限制条件:会给定先倒入哪个桶,并且在倒的过程中,不能出现如下情况:给定先倒的桶空了,而另一个桶是满的。 例如题:https://exercism.io/my/solutions/3b849bba3ee840ccac12eb7ca734ba8e 问题分析 如果单纯针对这个问题来看,相信我们还是可以很容易的得到一个推导过程。既然我们有两个桶,一个是 3 升,一个是 5 升,那么我们可能需要将一个桶装满水,然后倒到另外一个桶里,通过将一个桶倒满而另外一个桶可能还有盈余来达到最终兑换出期望容量的水的目的。按照这个思路,我们可以开始第一步分析。 初步分析 上个例子问题中,我们整个兑水的过程可以描述如下(假设先倒入 3 升的桶): (3, 0)将 a 桶倒满; (0, 3) 将 a 桶倒入 b 桶;此情况可以出现,因为虽然 a 桶满了,但是 b 桶未满 (3, 3) 将 a 桶倒满; (1, 5) 将 a 桶倒入 b 桶...
- 下一篇
AI:拿来主义——预训练网络(一)
我们已经训练过几个神经网络了,识别手写数字,房价预测或者是区分猫和狗,那随之而来就有一个问题,这些训练出的网络怎么用,每个问题我都需要重新去训练网络吗?因为程序员都不太喜欢做重复的事情,因此答案肯定是已经有轮子了。 我们先来介绍一个数据集,ImageNet。这就不得不提一个大名鼎鼎的华裔 AI 科学家李飞飞。 2005 年左右,李飞飞结束了他的博士生涯,开始了他的学术研究不就她就意识到了一个问题,在此之前,人们都尽可能优化算法,认为无论数据如何,只要算法够好,就能做出更好的决策,李飞飞意识到了这个问题的局限性,恰巧她还是一个行动派,她要做出一个无比庞大的数据集,尽可能描述世界上一切物体的数据集,下载图片,给没一张图片做标注,简单而无聊,当然后来这项工作放到了亚马逊的众包平台上,全世界无数的人参与了这个伟大的项目,到此刻为止,已经有 14,197,122 张图片(一千四百万张),21841 个分类。在这个发展的过程中,人们也发现了这个数据集带来的成功远比预想的要多,甚至现在被认为最有前景的深度卷积神经网络的提出也与 ImageNet 不无关系。我忘记了谁这么说过:“就单单这一个数据集,就...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Red5直播服务器,属于Java语言的直播服务器
- CentOS6,CentOS7官方镜像安装Oracle11G
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装