Go语言:Go 语言之 defer 的前世今生( 五 )


在 SSA 阶段与在堆上分配的区别在于 , 在栈上创建 defer ,需要直接在函数调用帧上使用编译器来初始化 _defer 记录 , 并作为参数传递给 deferprocStack:
1// src/cmd/compile/internal/gc/ssa.go2func (s *state) call(n *Node, k callKind) *ssa.Value {3 ...4 var call *ssa.Value5 if k == callDeferStack {6 // 直接在栈上创建 defer 记录7 t := deferstruct(stksize) // 从编译器角度构造 _defer 结构8 d := tempAt(n.Pos, s.curfn, t)910 s.vars[&memVar] = s.newValue1A(ssa.OpVarDef, types.TypeMem, d, s.mem)11 addr := s.addr(d, false)1213 // 在栈上预留记录 _defer 的各个字段的空间14 s.store(types.Types[TUINT32],15 s.newValue1I(ssa.OpOffPtr, types.Types[TUINT32].PtrTo, t.FieldOff(0), addr),16 s.constInt32(types.Types[TUINT32], int32(stksize)))17 s.store(closure.Type,18 s.newValue1I(ssa.OpOffPtr, closure.Type.PtrTo, t.FieldOff(6), addr),19 closure)2021 // 记录参与 defer 调用的函数参数22 ft := fn.Type23 off := t.FieldOff(12)24 args := n.Rlist.Slice2526 // 调用 deferprocStack , 以 _defer 记录的指针作为参数传递27 arg0 := s.constOffPtrSP(types.Types[TUINTPTR], Ctxt.FixedFrameSize)28 s.store(types.Types[TUINTPTR], arg0, addr)29 call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferprocStack, s.mem)30 ...31 } else { ... }3233 // 函数尾声与堆上分配的栈一样 , 调用 deferreturn34 if k == callDefer || k == callDeferStack {35 ...36 s.exit37 }38 ...39 } 可见 , 在编译阶段 , 一个 _defer 记录的空间已经在栈上得到保留 , deferprocStack 的作用就仅仅承担了运行时对该记录的初始化这一功能:
1// src/runtime/panic.go23//go:nosplit4func deferprocStack(d *_defer) {5 gp := getg6 // 注意 , siz 和 fn 已经在编译阶段完成设置 , 这里只初始化了其他字段7 d.started = false8 d.heap = false // 可见此时 defer 被标记为不在堆上分配9 d.openDefer = false10 d.sp = getcallersp11 d.pc = getcallerpc12 ...13 // 尽管在栈上进行分配 , 仍然需要将多个 _defer 记录通过链表进行串联 , 14 // 以便在 deferreturn 中找到被延迟的函数的入口地址:15 // d.link = gp._defer16 // gp._defer = d17 *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))18 *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))19 return020 } 至于函数尾声的行为 , 与在堆上进行分配的操作同样是调用 deferreturn , 我们就不再重复说明了 。 当然 , 里面涉及的 freedefer 调用由于不需要释放任何内存 , 也就早早返回了:
1// src/runtime/panic.go2func freedefer(d *_defer) {3 if !d.heap { return }4 ...5}
Go语言:Go 语言之 defer 的前世今生
本文插图
开放编码式 defer 正如本文最初所描述的那样 , defer 给我们的第一感觉其实是一个编译期特性 。 前面我们讨论了为什么 defer 会需要运行时的支持 , 以及需要运行时的 defer 是如何工作的 。 现在我们来探究一下什么情况下能够让 defer 进化为一个仅编译期特性 , 即在函数末尾直接对延迟函数进行调用 , 做到几乎不需要额外的开销 。 这类几乎不需要额外运行时性能开销的 defer , 正是开放编码式 defer 。 这类 defer 与直接调用产生的性能差异有多大呢?我们不妨编写两个性能测试:
1func call { func {} }2func callDefer { defer func {} }3func BenchmarkDefer(b *testing.B) {4 for i := 0; i < b.N; i++ {5 call // 第二次运行时替换为 callDefer6 }7}