关于defer我们知道它会在函数返回之前,倒序执行。
1
2
3
4
func A() {
defer B()
//code to to something
}
像这样的一段代码,编译后的伪指令是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func A() {
//第一个参数是defer函数的参数加返回值共占用多大空间
//第二个参数是一个funcval
r := deferproc(8, B) //1. 注册
if r > 0 {
goto ret
}
//code to do something
runtime.deferreturn() //2. 执行
return
ret:
runtime.deferreturn
}
defer 指令对应到两部分内容,deferproc 负责把要执行的函数信息保存起来,我们称之为defer注册, deferproc 函数会返回0。if r > 0 { goto ret } 这个分支和 panic 和 recover 有关,我们先忽略不看。这样程序执行的逻辑更清晰了,defer 注册完成后继续执行后面的逻辑,知道返回之前通过 deferreturn 执行注册的 defer 函数。正是因为先注册后调用才实现了defer延迟执行的效果。
defer 信息会注册到一个链表,而当前执行的goroutine持有这个链表的头指针。每个 goroutine 在运行时都有一个对应的结构体 g,其中有一个字段指向defer链表头,defer链表链起来的是一个个的 _defer 结构体。新注册的defer会添加到链表头,执行时也是从头开始,执行后移除链表项。所以defer才会表现为倒序执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//src/runtime/runtime2.go
type g struct {
...
_defer *_defer
...
}
type _defer struct {
siz int32 // 参数和返回值共占多少字节
started bool //标记defer是否已经执行
heap bool //是否为堆分配
openDefer bool //go1.14增加
sp uintptr //记录注册这个defer函数栈指针,通过它函数可以判断自己注册的defer是否已经执行完了
pc uintptr //deferproc的返回地址
fn *funcval //要注册的函数 function value
_panic *_panic // panic that is running defer
link *_defer //链接到前一个注册的_defer结构体
fd unsafe.Pointer //go1.14增加
varp uintptr //go1.14增加
framepc uintptr//go1.14增加
}
看一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
func A1(a int) {
fmt.Println(a) //1
}
func main() {
a1, b1 := 1, 2
defer A1(a1)
a1 = a1 + b1
fmt.Println(a1, b1) //3 2
}
//Output:
//3 2
//1
deferproc函数调用时,编译器会在它自己的两个参数后面开辟一段空间,用于存放defer函数的返回值和参数,这一段空间会被直接拷贝到_defer结构体的后面。A1只有一个参数,放在deferproc函数自己的两个参数后面。
deferproc函数执行时,需要堆分配一段空间,用于存放_defer结构体,以及siz大小的参数与返回值。
然后代码执行到deferreturn执行defer链表,从当前goroutine拿到链表头上的_defer结构体,通过fn找到funcval,拿到函数入口地址。
调用A1时会把_defer后面的参数与返回值整个拷贝到A1调用者栈上,然后A1开始执行输出1。这里的关键是defer函数的参数在注册时拷贝到堆上,执行时又拷贝到栈上。
既然deferproc注册的是一个funcval,再来看看有捕获列表时是什么情况。
1
2
3
4
5
6
7
8
9
10
11
12
func A() {
a, b := 1, 2
defer func(b int) {
a = a + b
fmt.Println(a, b) //5 2
}(b)
a = a + b
fmt.Println(a, b) //3 2
}
//Output:
//3 2
//
这个例子中defer函数不止要传递局部变量b做参数,还捕获了外层函数局部变量a,形成闭包。
执行阶段会创建闭包对象:由于捕获局部变量a,除了初始化赋值外还被修改过,所以局部变量a改为堆分配。栈上存储它的地址还有局部变量b=2。然后创建闭包对象,堆分配一个funcval结构体,捕获列表中存储a的地址。deferproc执行时_defer结构体中fn保存的就是这个funcval结构体的起始地址。
这里最关键是分清楚defer传参与闭包捕获变量的实现机制。
以上是Go1.12版本defer的基本设计思路,这一版本的defer比较明显的问题就是慢。 第一个原因是_defer结构体堆分配,即使有预分配的deferpool也需要去堆上获取与释放,而且参数还要在堆栈间来回拷贝。 第二个原因是使用链表注册defer信息,而链表本身操作比较慢,所以Go1.13中和Go1.14中分别做了不同的优化。
优化
Go1.13和Go1.14中如何改进,在来看下这段代码在1.12和1.13中编译后的伪指令:
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
func A() {
defer B(10)
//code to do something
}
func B(i int){
}
//Go1.13
func A() {
r := runtime.deferproc(0, B, 10)
if r > 0 {
goto ret
}
//code to do something
runtime.deferreturn()
return
ret:
runtime.deferreturn
}
//Go1.13
func A() {
var d struct {
runtime._defer
i int
}
d.siz = 0
d.fn = B
d.i = 10
r := runtime.deferprocStack(&d._defer)
if r > 0 {
goto ret
}
//code to do something
runtime.deferreturn()
return
ret:
runtime.deferreturn
}
Go1.13
1.12中通过deferproc注册defer函数信息, _defer结构体分配在堆上。 1.13中通过在编译阶段增加局部变量,把_defer信息保存到当前函数栈帧的局部变量区域,再通过deferprocStack把栈上这个_defer结构体注册到defer链表中。
defer 在 1.13的优化点主要在减少defer信息的堆分配。之所以说是减少是因为在显示或隐示循环中的defer依然要使用Go1.12版本的处理方式在堆上分配。为此_defer结构体增加了一个 heap 字段,用于标识是否为堆分配。defer函数执行时依然是通过deferreturn实现的,也同样要在defer函数是执行拷贝参数,不过不是在堆栈间,而是从栈上的局部变量空间拷贝到参数空间。
Go1.14
Go1.13的defer官方提供的性能提升是30%,那Go1.14版本又有什么不一样的优化策略呢?再来看下编译后的伪指令:
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
func A(i int) {
defer A1(i, 2*i)
//code to do something
if i > 1 {
defer A2("hello", "world")
}
//code to do something
return
}
func A1(a, b int) {
...
}
func A2(m, n string) {
...
}
//Go1.14
func A(i int) {
var df byte //df里每一位对应标识一个defer是否要被执行
var a, b int = i, 2 * i
//code to do something
var m, n string = "hello", "world"
df |= 1
if i > 1 {
df |= 2
}
//code to do something
if df & 2 > 0 {
df = df &^ 2
A2(a, b)
}
if df & 1 {
df = df &^ 1 //执行前第一位置为0,避免重复执行
A1(a, b)
}
return
... //省略panic recover相关
}
这里有两个defer,先看A1,这里把defer函数A1需要的参数,定义伪局部变量,然后再在函数返回前直接调用defer函数A1,用这样的方式省去了构造defer链表项并注册到链表的过程,也同样实现了defer函数延迟执行的效果。
不过A2就不能这样简单处理了,它要到执行阶段才能确定是否要被调用。Go语言用一个标识变量df来解决这个问题。
1
var df byte
df里每一位对应标识一个defer是否要被执行。这个例子里第一位对应defer函数A1,A1需要执行,所以通过『或』运算把df第一位置为1。
defer函数调用这里,先判断defer标识为是否为1,执行前还要把df对应标识位置为0,避免重复执行。然后直接调用A1就好。
同样的方式到defer A2这里,程序执行阶段会根据具体条件判断df第二个标志位是否要被置为1。对应的函数返回前也要依据第二个标志位来觉得是否要调用函数A2。
Go1.14的 defer 就是通过在编译阶段插入代码,把defer函数的执行逻辑展开在所属函数内,从而免于创建_defer结构体,而且不需要注册到defer链表。
Go语言称这种方式为 open coded defer。但是同1.13中的一样依然不适用于循环中的defer。所以1.12版本中的处理方式目前是一直保留的。
panic异常
一般情况下在循环中使用defer的情况应该比较少,Go1.14版本的defer几乎提升了一个数量级。但是这并非没有代价,我们上面提到的都是程序正常执行的流程,如果我们的代码中发生panic或者调用runtime.Goexit()函数,后面的代码基本就执行不到,就要去执行defer链表了。而这些 open coded 方式实现的defer并没有注册到链表,需要额外通过栈扫描的方式来发现。
所以Go1.14版本中的_defer结构体在Go1.13版本的基础上又增加了几个字段:
1
2
3
4
5
6
7
8
type _defer struct {
...
openDefer bool
fd unsafe.Pointer
varp uintptr
framepc uintptr
}
借助这些信息,可以找到未注册链表的defer函数,并按照正确的顺序执行。
所以Go1.14版本中defer的确变快了,但是panic变的更慢了。不过Go语言选择做出这样的优化定然是综合考量了整体性能,毕竟panic发生的几率要比defer低。