专栏首页面向人生编程探究 Go 语言 defer 语句的三种机制

探究 Go 语言 defer 语句的三种机制

Golang 的 1.13 版本 与 1.14 版本对 defer 进行了两次优化,使得 defer 的性能开销在大部分场景下都得到大幅降低,其中到底经历了什么原理?

这是因为这两个版本对 defer 各加入了一项新的机制,使得 defer 语句在编译时,编译器会根据不同版本与情况,对每个 defer 选择不同的机制,以更轻量的方式运行调用。

堆上分配

在 Golang 1.13 之前的版本中,所有 defer 都是在堆上分配,该机制在编译时会进行两个步骤:

1.在 defer 语句的位置插入 runtime.deferproc,当被执行时,延迟调用会被保存为一个 _defer 记录,并将被延迟调用的入口地址及其参数复制保存,存入 Goroutine 的调用链表中。2.在函数返回之前的位置插入 runtime.deferreturn,当被执行时,会将延迟调用从 Goroutine 链表中取出并执行,多个延迟调用则以 jmpdefer 尾递归调用方式连续执行。

这种机制的主要性能问题存在于每个 defer 语句产生记录时的内存分配,以及记录参数和完成调用时参数移动的系统调用开销。

栈上分配

Go 1.13 版本新加入 deferprocStack 实现了在栈上分配的形式来取代 deferproc,相比后者,栈上分配在函数返回后 _defer 便得到释放,省去了内存分配时产生的性能开销,只需适当维护 _defer 的链表即可。

编译器有自己的逻辑去选择使用 deferproc 还是 deferprocStack,大部分情况下都会使用后者,性能会提升约 30%。不过在 defer 语句出现在了循环语句里,或者无法执行更高阶的编译器优化时,亦或者同一个函数中使用了过多的 defer 时,依然会使用 deferproc

开放编码

Go 1.14 版本继续加入了开发编码(open coded),该机制会将延迟调用直接插入函数返回之前,省去了运行时的 deferprocdeferprocStack 操作,在运行时的 deferreturn 也不会进行尾递归调用,而是直接在一个循环中遍历所有延迟函数执行。

这种机制使得 defer开销几乎可以忽略,唯一的运行时成本就是存储参与延迟调用的相关信息,不过使用此机制需要一些条件:

1.没有禁用编译器优化,即没有设置 -gcflags "-N";2.函数内 defer 的数量不超过 8 个,且返回语句与延迟语句个数的乘积不超过 15;3.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,不调用 f2if deferBits & 1 << 1!= 0{    deferBits &^= 1 << 1  // 移位为下次判断准备    _f2(_a2)}// 同理,由于 00000001 & 00000001 != 0,调用 f1if 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

本文分享自微信公众号 - 面向人生编程(LifeOriented),作者:张凯强

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-03-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 用Go语言异常机制模拟TryCatch异常捕捉!

    有的同学看到Go和TryCatch一起出现,心里可能会说,难道Go语言升级了,加入了try...catch语句。哈哈,其实Go语言从创建之初就没打算加入try....

    PHP开发工程师
  • defer 的前世今生

    延迟语句 defer 在最早期的 Go 语言设计中并不存在,后来才单独增加了这一特性,由 Robert Griesemer 完成语言规范的编写 [Griesem...

    梦醒人间
  • Go 语言简介(上)— 语法

    Hello World package main //声明本文件的package名 import "fmt" //import语言的fmt库——用于输出 f...

    李海彬
  • 牛客网_Go语言相关练习_判断&选择题(4)

    错误指的是可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况在人们的意料之中;而异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况...

    Zoctopus
  • Go语言错误处理

    错误指的是可能出现问题的地方出现了问题,比如打开一个文件时可能失败,这种情况在人们的意料之中。

    Steve Wang
  • Defer,Panic,and Recover

    Go拥有一般的控制流程机制,像if、for、switch、goto。除此之外go也拥有一个单独的goroutine机制运行go语句。这里我想讨论一些不太常见的语...

    陌无崖
  • 你听说过Go语言的TryCatch吗?

    有的同学看到Go和TryCatch一起出现,心里可能会说,难道Go语言升级了,加入了try...catch语句。哈哈,其实Go语言从创建之初就没打算加入try....

    蒙娜丽宁
  • GO 语言简介 — 语法

    下面的文章主要是以代码和注释为主。只需要你对C语言,Unix,Python有一点基础,我相信你会在30分钟左右读完并对Go语言有一些初步了解的。

    禹都一只猫olei
  • Golang语言--细节汇总

    slice和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用...自动 计算长度,而声明slice时,方括号内没有任何字符。 对于slice有几个...

    李海彬

扫码关注云+社区

领取腾讯云代金券