0%

《Golang》Defer的实现原理

Defer原理

首先,我们要确定 defer 关键字被编译器翻译成了哪个函数

看下面的代码:

1
2
3
4
5
6
7
8
package main

func main() {
defer func() {
recover()
}()
panic("error")
}

我们把它编译出来,然后看看汇编代码就知道了

编译

1
go build -gcflags=all="-N -l" ./bin/main/   // -N禁止编译器优化,-l禁止内联编译

打印汇编

1
go tool objdump -s "main.main" main

或者直接

1
go tool compile -S ./bin/main/main.go

打印之后是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
TEXT main.main(SB) /Users/joy/Work/backend/golang/golang-test/bin/main/main.go   // main函数
MOVQ GS:0x30, CX
CMPQ 0x10(CX), SP
JBE 0x105ea81 // 检查栈大小,小了就跳转到morestack执行
SUBQ $0x68, SP // 下面是设置d对象的siz以及fn
MOVQ BP, 0x60(SP)
LEAQ 0x60(SP), BP
MOVL $0x0, 0x10(SP)
LEAQ go.func.*+85(SB), AX
MOVQ AX, 0x28(SP)
LEAQ 0x10(SP), AX
MOVQ AX, 0(SP)
CALL runtime.deferprocStack(SB) // 调用deferprocStack函数
TESTL AX, AX // 如果返回不是0,则直接跳转到deferreturn执行
JNE 0x105ea71
JMP 0x105ea55
LEAQ type.*+37376(SB), AX
MOVQ AX, 0(SP)
LEAQ runtime.checkASM.args_stackmap+24(SB), AX
MOVQ AX, 0x8(SP)
CALL runtime.gopanic(SB) // 调用panic函数
NOPL
CALL runtime.deferreturn(SB) // 递归执行所有defer中的函数
MOVQ 0x60(SP), BP
ADDQ $0x68, SP
RET // main返回
CALL runtime.morestack_noctxt(SB)
JMP main.main(SB)
INT $0x3
INT $0x3
INT $0x3
INT $0x3
INT $0x3
INT $0x3
INT $0x3
INT $0x3

TEXT main.main.func1(SB) /Users/joy/Work/backend/golang/golang-test/bin/main/main.go // defer中的那个匿名函数
MOVQ GS:0x30, CX
CMPQ 0x10(CX), SP
JBE 0x105eac5
SUBQ $0x20, SP
MOVQ BP, 0x18(SP)
LEAQ 0x18(SP), BP
LEAQ 0x28(SP), AX
MOVQ AX, 0(SP)
CALL runtime.gorecover(SB)
MOVQ 0x18(SP), BP
ADDQ $0x20, SP
RET
CALL runtime.morestack_noctxt(SB)
JMP main.main.func1(SB)

可以看到关键字 defer 被翻译成了 runtime.deferprocStack函数(其实这里不一定,详情查看本篇扩展),而fn参数的分配也被直接编译在了汇编代码中

有n个defer,就会翻译成n个runtime.deferprocStack函数,放在对应的位置上

下面看看runtime.deferprocStack函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func deferprocStack(d *_defer) {
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
// siz and fn are already set.
// The other fields are junk on entry to deferprocStack and
// are initialized here.
d.started = false
d.heap = false // 不是分配在堆上
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0
// The lines below implement:
// d.panic = nil
// d.fd = nil
// d.link = gp._defer
// gp._defer = d
// But without write barriers. The first three are writes to
// the stack so they don't need a write barrier, and furthermore
// are to uninitialized memory, so they must not use a write barrier.
// The fourth write does not require a write barrier because we
// explicitly mark all the defer structures, so we don't need to
// keep track of pointers to them with a write barrier.
*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer)) // 链向上一个defer。所以其实维护了一个defer链表
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d)) // 将d结构放入当前g的defer变量中,代表下一个运行的defer是这个

return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}

函数执行完后,就会依次执行 deferreturn 函数了,我们看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer // 取出g的defer变量中的defer,也就是链表最后一个defer。所以defer执行顺序是栈的后进先出的逻辑
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
return
}
if d.openDefer {
done := runOpenDeferFrame(gp, d)
if !done {
throw("unfinished open-coded defers in deferreturn")
}
gp._defer = d.link
freedefer(d)
return
}

// Moving arguments around.
//
// Everything called after this point must be recursively
// nosplit because the garbage collector won't know the form
// of the arguments until the jmpdefer can flip the PC over to
// fn.
switch d.siz { // 这里取出defer函数的所有参数
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
gp._defer = d.link // 下一个要执行的defer放入g中
freedefer(d) // 清理defer对象
// If the defer function pointer is nil, force the seg fault to happen
// here rather than in jmpdefer. gentraceback() throws an error if it is
// called with a callback on an LR architecture and jmpdefer is on the
// stack, because the stack trace can be incorrect in that case - see
// issue #8153).
_ = fn.fn
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) // 带上参数,跳到defer函数去执行,并且会继续跳到deferreturn处递归执行,直到执行完所有的defer函数
}

下面分析下jmpdefer汇编实现(很巧妙的实现了defer函数的递归调用):

1
2
3
4
5
6
7
8
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
MOVQ fv+0(FP), DX // fn 第一个参数fn放入DX寄存器
MOVQ argp+8(FP), BX // caller sp jmpdefer 的第二个参数放到BX寄存器,jmpdefer 的第二个参数就是deferreturn的第一个参数的指针
LEAQ -8(BX), SP // caller sp after CALL 因为返回地址是紧跟着被调用者的第一个参数的后面,所以-8(BX)就是deferreturn的调用者的返回地址,这里恢复出来了
MOVQ -8(SP), BP // restore BP as if deferreturn returned (harmless if framepointers not in use) -8(SP)就是deferreturn的调用者的栈基了,这里把它恢复出来。到这里栈帧已经恢复到deferreturn的调用者了。意味着后面的fn是跟deferreturn在同级执行了
SUBQ $5, (SP) // return to CALL again 将SP寄存器中的值(deferreturn的调用者的栈顶元素,也就是下一个函数调用的返回地址)减去5,也就是CALL的长度。就会导致下面的fn执行完后,又跳回到了CALL指令,实现了递归
MOVQ 0(DX), BX // fn地址拿出来
JMP BX // but first run the deferred function 跳转到fn去执行fn的代码

扩展

编译器对defer关键字的处理

go1.13之前,编译器将defer关键字直接翻译成 runtime.deferproc函数

而在go1.13或之后的版本中,编译器会条件性的翻译成 runtime.deferproc函数 或者 runtime.deferprocStack函数

runtime.deferproc函数 是将defer分配在堆上,而runtime.deferprocStack函数则是分配在栈上,能大幅提高defer效率

那编译器是怎么选择翻译成哪个函数呢?

查看编译器源码

src/cmd/compile/internal/gc/esc.go

1
2
3
4
5
case ODEFER:
if e.loopdepth == 1 { // top level 循环深度是1,也就是没有循环,则分配到栈上
n.Esc = EscNever // force stack allocation of defer record (see ssa.go)
break
}

src/cmd/compile/internal/gc/ssa.go

1
2
3
4
5
6
case ODEFER:
d := callDefer
if n.Esc == EscNever {
d = callDeferStack
}
s.call(n.Left, d)

所以如果像下面这种代码,就会分配到堆上,就会影响性能。可以自行编译后看看汇编代码验证一下

1
2
3
4
5
6
7
func main() {
for p := 0; p < 10; p++ {
defer func() {
fmt.Println(1)
}()
}
}

总结

  1. defer会被条件性的编译成 runtime.deferproc函数 或者 runtime.deferprocStack函数,以达到提高效率的目的
  2. deferreturn函数实现了g下所有defer的递归调用

下篇预告

Panic和Recover的实现原理




微信关注我,及时接收最新技术文章