探究 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
本文属于原创,首发于微信公众号「面向人生编程」,如需转载请后台留言。
关注后回复以下信息获取更多资源
回复【资料】获取 Python / Java 等学习资源
回复【插件】获取爬虫常用的 Chrome 插件
回复【知乎】获取最新知乎模拟登录
探究 Go 语言 defer 语句的三种机制的更多相关文章
- PHP语言学习之php-fpm 三种运行模式
本文主要向大家介绍了PHP语言学习之php-fpm 三种运行模式,通过具体的内容向大家展示,希望对大家学习php语言有所帮助. php-fpm配置 配置文件:php-fpm.conf 开启慢日志功能的 ...
- insert into 语句的三种写法
insert into 语句的三种写法 方式1. INSERT INTO t1(field1,field2) VALUES (v001,v002); // 明确只插入一条Valu ...
- C语言/C++编程学习三种循环用法和区别
C语言是面向过程的,而C++是面向对象的 C和C++的区别: C是一个结构化语言,它的重点在于算法和数据结构.C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现 ...
- JVM探究 面试题 JVM的位置 三种JVM:HotSpot 新生区 Young/ New 养老区 Old 永久区 Perm 堆内存调优GC的算法有哪些?标记清除法,标记压缩,复制算法,引用计数法
JVM探究 面试题: 请你弹弹你对JVM的理解?Java8虚拟机和之前的变化更新? 什么是OOM?什么是栈溢出StackOverFlowError?怎么分析 JVM的常用调优参数有哪些? 内存快照如何 ...
- switch语句以及三种循环语句的总结
1:switch语句(1)格式:switch(表达式) {case 值1:语句体1;break;case 值2:语句体2;break;...default:语句体n+1;break;} 格式解释说明: ...
- [SQL分页语句的三种方式]
我们在开发的过程经常会用到数据分页,在网上也可以搜到大量的分页插件.这是在端上控制的;有的是在SQL语句实现分页,这是在数据源上 实现分页的; 今天,我就在总结一下我经常用到的SQL语句分页! 第一种 ...
- CSS布局的三种机制
浮动元素之间没有缝隙,这和行内块还是不一样的,有点区别的! 2) 浮动元素与兄弟盒子之间的关系 注意:解决浮动的四种办法,后三种都是针对浮动元素的父元素的.
- java break语句的三种用法
1.用于switch语句当中,用于终止语句 2.用于跳出循环,此为不带标签的break语句,相当与goto的作用 e.g while(i<j&&h<k){ if(h< ...
- c 语言结构体的三种定义方式
struct 结构体名{ 成员列表: ..... }结构体变量: 结构体类型变量的定义 结构体类型变量的定义与其它类型的变量的定义是一样的,但由于结构体类型需要针对问题事先自行定义,所以结构体 ...
随机推荐
- 搭建公司的React开发环境
记录公司环境搭建 1.安装VSCODE, 安装网上的推荐各种控件2.安装node, yarn, 会自动添加path3.先初始化npm 全部按回车默认. npm init. 初始化yarn: yarn ...
- 用命令修改Oracle数据库密码
1.改密码 (1).打开doc命令框键入:sqlplus /nolog (2).输入:connect / as sysdba (3).修改密码:alter user userNa ...
- 十九、linux--RAID详解
一.什么是RADI Raid是廉价冗余磁盘阵列,简称磁盘阵列. 运维人员就叫RAID.Raid是一种把多块独立的磁盘(物理磁盘)按不同方式组合起来形成一个磁盘组,在逻辑上看起来就是一个大的磁盘,从而提 ...
- LeetCode No.103,104,105
No.103 ZigzagLevelOrder 二叉树的锯齿形层次遍历 题目 给定一个二叉树,返回其节点值的锯齿形层次遍历.(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行). ...
- PAT甲级——1011 World Cup Betting
PATA1011 World Cup Betting With the 2010 FIFA World Cup running, football fans the world over were b ...
- Docker系列七: 使用Humpback管理工具管理容器(一款UI管理工具)
Humpback 可以帮助企业快速搭建轻量级的 Docker 容器云管理平台,若将你的 Docker 主机接入到 Humpback 平台中,就能够为你带来更快捷稳定的容器操作体验. 功能特点 Web操 ...
- kubectl格式化输出和调试
1.格式化输出 以特定的格式想终端输出详细信息,可以在 kubectl 命令中添加 -o 或者 -output 选项 输出格式 描述 -o=custom-columns=<spec> 使 ...
- Tukey法
Tukey法 在介绍Tukey方法前,首先了解学生化极差分布. 在概率论和统计学中,学生化极差分布是极差的抽样分布.该分布是一种连续型概率分布,用于在样本量较小且总体标准差未知的情况下估计正态分布总体 ...
- 透过微软研究院在“数据中心网络”的重大进展看SIGCOMM 2013
2013" title="透过微软研究院在"数据中心网络"的重大进展看SIGCOMM 2013"> 编者按:于8月12日至16日在香港举行的SI ...
- bzoj1076 奖励关(概率dp)(状态压缩)
BZOJ 1076 [SCOI2008]奖励关 Description 你正在玩你最喜欢的电子游戏,并且刚刚进入一个奖励关.在这个奖励关里,系统将依次随机抛出k次宝物,每次你都可以选择吃或者不吃(必须 ...