Golang 的 1.13 版本 与 1.14 版本对 defer
进行了两次优化,使得 defer
的性能开销在大部分场景下都获得大幅下降,其中到底经历了什么原理?git
这是由于这两个版本对 defer
各加入了一项新的机制,使得 defer
语句在编译时,编译器会根据不一样版本与状况,对每一个 defer
选择不一样的机制,以更轻量的方式运行调用。github
在 Golang 1.13 以前的版本中,全部 defer
都是在堆上分配,该机制在编译时会进行两个步骤:golang
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 则调用延迟函数,不然跳过。ui
为了轻量,官方将延迟比特限制为 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 语言本来:
changkun.de/golang/zh-c…
[2] 峰云就她了 - go1.14实现defer性能大幅度提高原理:
xiaorui.cc/archives/65…
[3] 34481-opencoded-defers:
github.com/golang/prop…
本文属于原创,首发于微信公众号「面向人生编程」,如需转载请后台留言。