5.3 defer
5.3.1 现象
使用defer
时会出现两个常见的问题:
defer
的调用时机和调用顺序defer
会使用值传递的方式传参时进行预计算,导致预期结果不符
作用域
defer
语句会在函数返回之前执行,并按照后进先出的顺序执行。
func main() {
for i := 0; i < 5; i++ {
defer println(i)
}
}
$ go run main.go
4
3
2
1
0
上述代码可以看出,defer
的执行顺序是按照后进先出的顺序的。
func main() {
{
defer println("defer runs")
println("block ends")
}
println("func ends")
}
$ go run main.go
block ends
main ends
defer runs
可以看出,defer
是在函数返回之前被调用。
预计算参数
func main() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt))
time.Sleep(time.Second)
}
$ go run main.go
0s
调用 defer
关键字会立刻拷贝函数中引用的外部参数, time.Since(startedAt)
的结果不是在 main
函数退出之前计算的,而是在 defer
关键字调用时计算的,最终导致上述代码输出 0s。
若使用defer
调用匿名函数:
func main() {
startedAt := time.Now()
defer func() { fmt.Println(time.Since(startedAt)) }()
time.Sleep(time.Second)
}
$ go run main.go
1s
此时调用defer
时,拷贝的是函数的指针,对函数内部的参数没有影响,可得到预期的结果。
5.3.2 数据结构
defer
的数据结构用runtime._defer
表示,主要字段如下:
type _defer struct {
siz int32
started bool
openDefer bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
...
}
siz
:参数和结果的内存大小sp
:栈指针pc
:程序计数器fn
:defer
关键字中传入的函数_panic
:触发延迟调用的结构体openDefer
:当前defer
是否经过开放编码优化link
:下一个_defer
节点
runtime._defer
之间使用链表的形式串联起来:
5.3.3 执行机制
中间代码生成阶段会使用cmd/compile/internal/gc.state.stmt
根据三种不同的情况进行处理:
- 堆分配
- 栈分配
- 开放编码
func (s *state) stmt(n *Node) {
...
switch n.Op {
case ODEFER:
if s.hasOpenDefers {
s.openDeferRecord(n.Left) // 开放编码
} else {
d := callDefer // 堆分配
if n.Esc == EscNever {
d = callDeferStack // 栈分配
}
s.callResult(n.Left, d)
}
}
}
5.3.4 堆上分配
当使用堆分配时,编译器会调用 cmd/compile/internal/gc.state.callResult
和 cmd/compile/internal/gc.state.call
进行处理(实际上defer
是被按照函数调用进行处理的)。
cmd/compile/internal/gc.state.call
负责为所有函数和方法调用生成中间代码,流程如下:
- 获取需要执行的函数名、闭包指针、代码指针和函数调用的接收方
- 获取栈地址并将函数或者方法的参数写入栈中
- 使用
cmd/compile/internal/gc.state.newValue1A
以及相关函数生成函数调用的中间代码 - 若当前调用的函数是
defer
,那么会单独生成相关的结束代码块 - 获取函数的返回值地址并结束当前调用
func (s *state) call(n *Node, k callKind, returnResultAddr bool) *ssa.Value {
...
var call *ssa.Value
if k == callDeferStack {
// 在栈上初始化 defer 结构体
...
} else {
...
switch {
case k == callDefer:
aux := ssa.StaticAuxCall(deferproc, ACArgs, ACResults)
call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, aux, s.mem())
...
}
call.AuxInt = stksize
}
s.vars[&memVar] = call
...
}
编译器将 defer
关键字都转换成 runtime.deferproc
函数,并通过以下三个步骤为所有调用 defer
的函数末尾插入 runtime.deferreturn
的函数调用:
cmd/compile/internal/gc.walkstmt
在遇到ODEFER
节点时会执行Curfn.Func.SetHasDefer(true)
设置当前函数的hasdefer
属性cmd/compile/internal/gc.buildssa
会执行s.hasdefer = fn.Func.HasDefer()
更新state
的hasdefer
cmd/compile/internal/gc.state.exit
会根据state
的hasdefer
在函数返回之前插入runtime.deferreturn
的函数调用
func (s *state) exit() *ssa.Block {
if s.hasdefer {
...
s.rtcall(Deferreturn, true, nil)
}
...
}
上述两个运行时函数是 defer
关键字运行时机制的入口,分别承担了不同的工作:
runtime.deferproc
负责创建新的延迟调用;runtime.deferreturn
负责在函数调用结束时执行所有的延迟调用;
创建延迟调用
runtime.deferproc
为 defer
创建新的 runtime._defer
结构体、设置函数指针 fn
、程序计数器 pc
和栈指针 sp
并将相关的参数拷贝到相邻的内存空间中:
func deferproc(siz int32, fn *funcval) {
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()
}
最后调用的 runtime.return0
是唯一一个不会触发延迟调用的函数,可以避免递归 runtime.deferreturn
的递归调用。
runtime.deferproc
中 runtime.newdefer
的作用是获得 runtime._defer
结构体,包含三种路径:
- 从调度器的延迟调用缓存池
sched.deferpool
中取出结构体并将该结构体追加到当前 Goroutine 的缓存池中 - 从 Goroutine 的延迟调用缓存池
pp.deferpool
中取出结构体 - 通过
runtime.mallocgc
在堆上创建一个新的结构体
func newdefer(siz int32) *_defer {
var d *_defer
sc := deferclass(uintptr(siz))
gp := getg()
if sc < uintptr(len(p{}.deferpool)) {
pp := gp.m.p.ptr()
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
d := sched.deferpool[sc]
sched.deferpool[sc] = d.link
pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
}
if n := len(pp.deferpool[sc]); n > 0 {
d = pp.deferpool[sc][n-1]
pp.deferpool[sc][n-1] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-1]
}
}
if d == nil {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
}
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
三种方式最终都会将获取到 runtime._defer
结构体追加到所在 Goroutine _defer
链表的最前面。
可以看出defer
关键字的插入顺序是从后向前的,而 defer
关键字执行是从前向后的。
执行延迟调用
runtime.deferreturn
会从 Goroutine 的 _defer
链表中取出最前面的 runtime._defer
并调用 runtime.jmpdefer
传入需要执行的函数和参数:
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
...
switch d.siz {
case 0:
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
gp._defer = d.link
freedefer(d)
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
runtime.jmpdefer
主要工作是跳转到 defer
所在的代码段并在执行结束之后跳转回 runtime.deferreturn
:
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-8
MOVL fv+0(FP), DX // fn
MOVL argp+4(FP), BX // caller sp
LEAL -4(BX), SP // caller sp after CALL
#ifdef GOBUILDMODE_shared
SUBL $16, (SP) // return to CALL again
#else
SUBL $5, (SP) // return to CALL again
#endif
MOVL 0(DX), BX
JMP BX // but first run the deferred function
runtime.deferreturn
会多次判断当前 Goroutine 的 _defer
链表中是否有未执行的结构体,该函数只有在所有延迟函数都执行后才会返回。
5.3.5 栈上分配
当使用栈上分配时,可以节约内存分配带来的额外开销,与堆上分配的 runtime._defer
相比,该方法可以将 defer
关键字的额外开销降低 ~30%。。
在go v1.13
中,对 defer
关键字进行了优化,当该关键字在函数体中最多执行一次时,编译期间的 cmd/compile/internal/gc.state.call
会将结构体分配到栈上并调用 runtime.deferprocStack
:
func (s *state) call(n *Node, k callKind) *ssa.Value {
...
var call *ssa.Value
if k == callDeferStack {
// 在栈上创建 _defer 结构体
t := deferstruct(stksize)
...
ACArgs = append(ACArgs, ssa.Param{Type: types.Types[TUINTPTR], Offset: int32(Ctxt.FixedFrameSize())})
aux := ssa.StaticAuxCall(deferprocStack, ACArgs, ACResults) // 调用 deferprocStack
arg0 := s.constOffPtrSP(types.Types[TUINTPTR], Ctxt.FixedFrameSize())
s.store(types.Types[TUINTPTR], arg0, addr)
call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, aux, s.mem())
call.AuxInt = stksize
} else {
...
}
s.vars[&memVar] = call
...
}
在编译期间已经创建了 runtime._defer
结构体,所以在运行期间 runtime.deferprocStack
只需要设置一些未在编译期间初始化的字段,就可以将栈上的 runtime._defer
追加到函数的链表上:
func deferprocStack(d *_defer) {
gp := getg()
d.started = false
d.heap = false // 栈上分配的 _defer
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0
*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
return0()
}
5.3.5 开放编码
在go v1.14
之后,通过开放编码(Open Coded)实现defer
关键字,该设计使用代码内联优化 defer
关键的额外开销并引入函数数据 funcdata
管理 panic
的调用,该优化可以将 defer
的调用开销从 1.13 版本的 ~35ns 降低至 ~6ns 左右:
With normal (stack-allocated) defers only: 35.4 ns/op
With open-coded defers: 5.6 ns/op
Cost of function call alone (remove defer keyword): 4.4 ns/op
开放编码只会在满足以下的条件时启用:
- 函数的
defer
数量少于或者等于 8 个 - 函数的
defer
关键字不能在循环中执行 - 函数的
return
语句与defer
语句的乘积小于或者等于 15 个
启用优化
程序在编译期间会确定是否启用开放编码,编译器生成中间代码之前,会使用 cmd/compile/internal/gc.walkstmt
修改已经生成的抽象语法树,设置函数体上的 OpenCodedDeferDisallowed
属性:
const maxOpenDefers = 8
func walkstmt(n *Node) *Node {
switch n.Op {
case ODEFER:
Curfn.Func.SetHasDefer(true)
Curfn.Func.numDefers++
if Curfn.Func.numDefers > maxOpenDefers {
Curfn.Func.SetOpenCodedDeferDisallowed(true)
}
if n.Esc != EscNever {
Curfn.Func.SetOpenCodedDeferDisallowed(true)
}
fallthrough
...
}
}
在 SSA 中间代码生成阶段的 cmd/compile/internal/gc.buildssa
中,能够看到启用开放编码优化的其他条件,也就是返回语句的数量与 defer
数量的乘积需要小于 15:
func buildssa(fn *Node, worker int) *ssa.Func {
...
s.hasOpenDefers = s.hasdefer && !s.curfn.Func.OpenCodedDeferDisallowed()
...
if s.hasOpenDefers &&
s.curfn.Func.numReturns*s.curfn.Func.numDefers > 15 {
s.hasOpenDefers = false
}
...
}
中间代码生成的这两个步骤会决定当前函数是否应该使用开放编码优化 defer
关键字,一旦确定使用开放编码,就会在编译期间初始化延迟比特和延迟记录。
延迟记录
延迟比特和延迟记录是使用开放编码实现 defer
的两个最重要结构,一旦决定使用开放编码,cmd/compile/internal/gc.buildssa
会在编译期间在栈上初始化大小为 8 个比特的 deferBits
变量:
func buildssa(fn *Node, worker int) *ssa.Func {
...
if s.hasOpenDefers {
deferBitsTemp := tempAt(src.NoXPos, s.curfn, types.Types[TUINT8]) // 初始化延迟比特
s.deferBitsTemp = deferBitsTemp
startDeferBits := s.entryNewValue0(ssa.OpConst8, types.Types[TUINT8])
s.vars[&deferBitsVar] = startDeferBits
s.deferBitsAddr = s.addr(deferBitsTemp)
s.store(types.Types[TUINT8], s.deferBitsAddr, startDeferBits)
s.vars[&memVar] = s.newValue1Apos(ssa.OpVarLive, types.TypeMem, deferBitsTemp, s.mem(), false)
}
}
延迟比特中的每一个比特位都表示该位对应的 defer
关键字是否需要被执行,如下图所示,其中 8 个比特的倒数第二个比特在函数返回前被设置成了 1,那么该比特位对应的函数会在函数返回前执行:
因为不是函数中所有的 defer
语句都会在函数返回前执行,如下所示的代码只会在 if
语句的条件为真时,其中的 defer
语句才会在结尾被执行
deferBits := 0 // 初始化 deferBits
_f1, _a1 := f1, a1 // 保存函数以及参数
deferBits |= 1 << 0 // 将 deferBits 最后一位置位 1
if condition {
_f2, _a2 := f2, a2 // 保存函数以及参数
deferBits |= 1 << 1 // 将 deferBits 倒数第二位置位 1
}
exit:
if deferBits & 1 << 1 != 0 {
deferBits &^= 1 << 1
_f2(a2)
}
if deferBits & 1 << 0 != 0 {
deferBits &^= 1 << 0
_f1(a1)
}
延迟比特的作用就是标记哪些 defer
关键字在函数中被执行,这样在函数返回时可以根据对应 deferBits
的内容确定执行的函数,而正是因为 deferBits
的大小仅为 8 比特,所以该优化的启用条件为函数中的 defer
关键字少于 8 个。
上述伪代码展示了开放编码的实现原理,但是仍然缺少了一些细节,例如:传入 defer
关键字的函数和参数都会存储在如下所示的 cmd/compile/internal/gc.openDeferInfo
结构体中:
type openDeferInfo struct {
n *Node
closure *ssa.Value
closureNode *Node
rcvr *ssa.Value
rcvrNode *Node
argVals []*ssa.Value
argNodes []*Node
}
当编译器在调用 cmd/compile/internal/gc.buildssa
构建中间代码时会通过 cmd/compile/internal/gc.state.openDeferRecord
方法在栈上构建结构体,该结构体的 closure
中存储着调用的函数,rcvr
中存储着方法的接收者,而最后的 argVals
中存储了函数的参数。
很多 defer
语句都可以在编译期间判断是否被执行,若函数中的 defer
语句都会在编译期间确定,中间代码生成阶段就会直接调用 cmd/compile/internal/gc.state.openDeferExit
在函数返回前生成判断 deferBits
的代码,也就是上述伪代码中的后半部分。
不过当程序遇到运行时才能判断的条件语句时,我们仍然需要由运行时的 runtime.deferreturn
决定是否执行 defer
关键字:
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
sp := getcallersp()
if d.openDefer {
runOpenDeferFrame(gp, d)
gp._defer = d.link
freedefer(d)
return
}
...
}
该函数为开放编码做了特殊的优化,运行时会调用 runtime.runOpenDeferFrame
执行活跃的开放编码延迟函数,该函数会执行以下的工作:
- 从
runtime._defer
结构体中读取deferBits
、函数defer
数量等信息; - 在循环中依次读取函数的地址和参数信息并通过
deferBits
判断该函数是否需要被执行; - 调用
runtime.reflectcallSave
调用需要执行的defer
函数;
func runOpenDeferFrame(gp *g, d *_defer) bool {
fd := d.fd
...
deferBitsOffset, fd := readvarintUnsafe(fd)
nDefers, fd := readvarintUnsafe(fd)
deferBits := *(*uint8)(unsafe.Pointer(d.varp - uintptr(deferBitsOffset)))
for i := int(nDefers) - 1; i >= 0; i-- {
var argWidth, closureOffset, nArgs uint32 // 读取函数的地址和参数信息
argWidth, fd = readvarintUnsafe(fd)
closureOffset, fd = readvarintUnsafe(fd)
nArgs, fd = readvarintUnsafe(fd)
if deferBits&(1<<i) == 0 {
...
continue
}
closure := *(**funcval)(unsafe.Pointer(d.varp - uintptr(closureOffset)))
d.fn = closure
...
deferBits = deferBits &^ (1 << i)
*(*uint8)(unsafe.Pointer(d.varp - uintptr(deferBitsOffset))) = deferBits
p := d._panic
reflectcallSave(p, unsafe.Pointer(closure), deferArgs, argWidth)
if p != nil && p.aborted {
break
}
d.fn = nil
memclrNoHeapPointers(deferArgs, uintptr(argWidth))
...
}
return done
}
5.3.7 小结
defer
关键字的实现有三种机制:
- 堆上分配 · 1.1 ~ 1.12
- 编译期将
defer
关键字转换成runtime.deferproc
并在调用defer
关键字的函数返回之前插入runtime.deferreturn
- 运行时调用
runtime.deferproc
会将一个新的runtime._defer
结构体追加到当前 Goroutine 的链表头 - 运行时调用
runtime.deferreturn
会从 Goroutine 的链表中取出runtime._defer
结构并依次执行
- 编译期将
- 栈上分配 · 1.13
- 当该关键字在函数体中最多执行一次时,编译期间的
cmd/compile/internal/gc.state.call
会将结构体分配到栈上并调用runtime.deferprocStack
- 当该关键字在函数体中最多执行一次时,编译期间的
- 开放编码 · 1.14
- 编译期间判断
defer
关键字、return
语句的个数确定是否开启开放编码优化 - 通过
deferBits
和cmd/compile/internal/gc.openDeferInfo
存储defer
关键字的相关信息 - 若
defer
关键字的执行可以在编译期间确定,会在函数返回前直接插入相应的代码,否则会由运行时的runtime.deferreturn
处理
- 编译期间判断
defer
执行顺序和预先计算的原理:
- 后调用的
defer
函数会先执行:- 后调用的
defer
函数会被追加到 Goroutine_defer
链表的最前面 - 运行
runtime._defer
时是从前到后依次执行
- 后调用的
- 函数的参数会被预先计算;
- 调用
runtime.deferproc
函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算
- 调用