Go语言:Go 语言之 defer 的前世今生


Go语言:Go 语言之 defer 的前世今生
本文插图
作者 | 欧长坤
来源 | 码农桃花源
延迟语句 defer 在最早期的 Go 语言设计中并不存在 , 后来才单独增加了这一特性 , 由 Robert Griesemer 完成语言规范的编写 [Griesemer, 2009] ,并由 Ken Thompson 完成最早期的实现 [Thompson, 2009] , 两人合作完成这一语言特性 。
defer 的语义表明 , 它会在函数返回、产生恐慌或者 runtime.Goexit 时被调用 。 直觉上看 , defer 应该由编译器直接将需要的函数调用插入到该调用的地方 , 似乎是一个编译期特性 , 不应该存在运行时性能问题 , 非常类似于 C++ 的 RAII 范式(当离开资源的作用域时 , 自动执行析构函数) 。 但实际情况是 , 由于 defer 并没有与其依赖资源挂钩 , 也允许在条件、循环语句中出现 , 从而不再是一个作用域相关的概念 , 这就是使得 defer 的语义变得相对复杂 。 在一些复杂情况下 , 无法在编译期决定存在多少个 defer 调用 。
例如 , 在一个执行次数不确定的 for 循环中 , defer 的执行次数是随机的:
1func randomDefers {2 rand.Seed(time.Now.UnixNano)3 for rand.Intn(100) > 42 {4 defer func {5 println("changkun.de/golang")6 }7 }8} 因而 defer 并不是免费的午餐 , 在一个复杂的调用中 , 当无法直接确定需要的产生的延迟调用的数量时 , 延迟语句将导致运行性能的下降 。 本文我们来讨论 defer 的实现本质及其对症下药的相关性能优化手段 。

  • defer 的类型
  • 在堆上分配的 defer
    • 编译阶段
    • 运行阶段
  • 在栈上创建 defer
  • 开放编码式 defer
    • 产生条件
    • 延迟比特
  • defer 的优化之路
  • 小结
  • 进一步阅读的参考文献

Go语言:Go 语言之 defer 的前世今生
本文插图
defer 的类型 延迟语句的文法产生式 DeferStmt -> "defer" Expression 的描述非常的简单 , 因而也很容易将其处理为语法树的形式 , 但我们这里更关心的其实是它语义背后的中间和目标代码的形式 。
在 《Go 语言原本》Go 程序编译流程 一节中我们提到过 , 在进行中间代码生成阶段 , 会通过 compileSSA 先调用 buildssa 为函数体生成 SSA 形式的函数 , 而后调用 genssa 将函数的 SSA 中间表示转换为具体的指令 。
Go 语言的语句在执行 buildssa 阶段中 , 会由 state.stmt 完成函数中各个语句的 SSA 处理 。
1// src/cmd/compile/internal/gc/ssa.go2func buildssa(fn *Node, worker int) *ssa.Func {3 var s state4 ...5 s.stmtList(fn.Nbody)6 ...7}8func (s *state) stmtList(l Nodes) {9 for _, n := range l.Slice { s.stmt(n) }10} 对于延迟语句而言 , 其中间表示会产生三种不同的延迟形式 ,第一种是最一般情况下的在堆上分配的延迟语句 , 第二种是允许在栈上分配的延迟语句 , 最后一种则是**开放编码式(Open-coded)**的延迟语句 。
1// src/cmd/compile/internal/gc/ssa.go2func (s *state) stmt(n *Node) {3 ...4 switch n.Op {5 case ODEFER:6 // 开放编码式 defer7 if s.hasOpenDefers {8 s.openDeferRecord(n.Left)9 } else {10 // 堆上分配的 defer11 d := callDefer12 if n.Esc == EscNever {13 // 栈上分配的 defer14 d = callDeferStack15 }16 s.call(n.Left, d)17 }18 case ...19 }20 ...21}