在go语言中,我们可以很方便的用defer关闭一个打开的文件、释放一个Redis连接,或者解锁一个Mutex。而且Go语言在设计上保证,即使发生panic,所有的defer调用也能够被执行。不过多个defer函数是按照定义顺序倒序执行的。
我们通过一个例子来解释defer函数的延迟与倒序。
func f1() {
defer A()
// code to do something
}
这样一段代码,在Go1.12中编译以后的伪指令是这样的:
func f1() {
r := runtime.deferproc(0, A) // 经过recover返回时r为1,否则为0
if r > 0 {
goto ret
}
// code to do something
runtime.deferreturn()
return
ret:
runtime.deferreturn()
}
其中与defer指令相关的有两个部分。第一部分是deferproc
,它负责保存要执行的函数信息,我们称之为defer注册
。
func deferproc(siz int32, fn *funcval)
从函数原型来看,deferproc
函数有两个参数,第一个是被注册的defer函数的参数加返回值共占多少字节;第二个参数是一个runtime.funcval
结构体的指针,也就是一个Function Value。
与defer指令相关的第二部分就是deferreturn
,它被编译器插入到函数返回以前调用,负责执行已经注册的defer函数。所以defer函数之所以能延迟到函数返回前执行,就是因为先注册,后调用。
再来看看defer函数为什么会倒序执行。
defer注册信息会保存到defer链表。每个goroutine在运行时都对应一个runtime.g
结构体,其中有一个_defer
字段,保存的就是defer链表的头指针。
deferproc新注册的defr信息会添加到链表头。deferreturn执行时也从链表头开始,所以defer才会表现为倒序执行。
defer链表链起来的是一个一个_defer结构体。
type _defer struct {
siz int32
started bool
sp uintptr // sp at time of defer
pc uintptr
fn *funcval
_panic *_panic // panic that is running defer
link *_defer
}
siz 由deferproc第一个参数传入,就是defer函数参数加返回值的总大小。
这段空间会直接分配在_defer结构体后面,用于在注册时保存给defer函数传入的参数,并在执行时直接拷贝到defer函数的调用者栈上。
started 标识defer函数是否已经开始执行;
sp 就是注册defer函数的函数栈指针;
pc 是deferproc函数返回后要继续执行的指令地址;
fn 由deferproc的第二个参数传入,也就是被注册的defer函数;
_panic 是触发defer函数执行的panic指针,正常流程执行defer时它就是nil;
link 自然是链到之前注册的那个_defer结构体。
这一篇我们只关注正常流程下defer函数的执行,不考虑panic或runtime.Goexit()的情况。
func A1(a int) {
fmt.Println(a)
}
func A() {
a, b := 1, 2
defer A1(a)
a = a + b
fmt.Println(a, b)
}
这里函数A注册了一个defer函数A1,在A的函数栈帧中,局部变量区域存储a=1,b=2。
到deferproc函数注册defer函数A1时。
第一个参数是A1的参数加返回值共占多少字节。A1没有返回值,64位下一个整型参数占用8字节。
第二个参数是函数A1。前面我们介绍过,没有捕获列表的Function Value,在编译阶段会做出优化,就是在只读数据段分配一个共用的funcval结构体。如下图中,函数A1的指令入口地址为addr1。在只读数据段分配的指向A1指令入口的funcval结构体地址为addr2,所以deferproc函数第二个参数就是addr2。
额外要注意的是,deferproc函数调用时,编译器会在它自己的两个参数后面,开辟一段空间,用于存放defer函数A1的返回值和参数。这一段空间会在注册defer时,直接拷贝到_defer结构体的后面。A1只有一个参数a=1,放在deferproc函数自己的两个参数之后。注意deferproc函数的返回值空间并没有分配在调用者栈上,而是放到了寄存器中,这和recover有关,且先忽略。
然后这个_defer结构体就被添加到defer链表头,deferproc注册结束。
注意:
频繁的堆分配势必影响性能,所以Go语言会预分配不同规格的deferpool,执行时从空闲_defer中取一个出来用。没有空闲的或者没有大小合适的,再进行堆分配。用完以后,再放回空闲_defer池。这样可以避免频繁的堆分配与回收。
deferproc结束后,接下来会执行到a=a+b这一步,所以,局部变量a被置为3。接下来会输出:a=3,b=2。
//函数A编译后的伪指令
func A() {
a, b := 1, 2
r := runtime.deferproc(8, A1,1)
if r > 0 {
goto ret
}
a = a + b
fmt.Println(a, b)//3,2
runtime.deferreturn()//执行defer链表
return
ret:
runtime.deferreturn()
}
然后就到deferreturn执行defer链表这里了。从当前goroutine找到链表头上的这个_defer结构体,通过_defer.fn找到defer函数的funcval结构体,进而拿到函数A1的入口地址。接下来就可以调用A1了。
调用A1时,会把_defer后面的参数与返回值整个拷贝到A1的调用者栈上。然后A1开始执行,输出参数值a=1。这个例子的关键,是defer函数的参数在注册时拷贝到堆上,执行时再拷贝到栈上。
既然deferproc注册的是一个Function Value,下面就来看看有捕获列表时是什么情况。
func A() {
a, b := 1, 2
defer func(b int) {
a = a+b
fmt.Println(a, b)
}(b)
a = a + b
fmt.Println(a, b)
}
这个例子中,defer函数不止要传递局部变量b
做参数,还捕获了外层函数的局部变量a
,形成闭包。匿名函数会由编译器按照A_func1
这样的形式命名。如下图所示,假设这个闭包函数的指令入口地址为addr1
。
上图中,由于捕获变量a除了初始化赋值外还被修改过,所以A的局部变量a改为堆分配,栈上只存它的地址。创建闭包对象时,会堆分配一个funcval
结构体,funcval.fn
指向闭包函数入口addr1
,捕获列表中存储a在堆上的地址。而这个funcval
结构体本身的地址addr2,就是deferproc
执行时,_defer结构体中的fn的值。别忘了,传给defer函数的参数b=2,也要拷贝到_defer结构体后面。
上图所示_defer结构体被添加到defer链表头以后,deferproc注册结束。继续执行后面的逻辑。到a=a+b这里,a被置为3。下一步输出a=3,b=2。
接下来,deferreturn执行注册的defer函数时,要把参数b拷贝到栈上的参数空间。还记得闭包函数执行时怎样找到对应的捕获列表吗?通过寄存器存储的funcval地址加上偏移,找到捕获变量a的地址。
//示例代码编译后的伪指令
func A() {
a := new(int)
*a = 1
b := 2
r := runtime.deferproc(8, A_func1,2)
if r > 0 {
goto ret
}
*a = *a + b
fmt.Println(*a, b)//3,2
runtime.deferreturn()//执行defer链表
return
ret:
runtime.deferreturn()
}
func A_func1(b int){
a := (int *)([DX]+8)
*a = *a + b
fmt.Println(*a,b)
}
要注意捕获变量a在堆上分配,闭包函数执行时,捕获变量a=3,参数b=2。
所以,接下来在defer函数中,捕获变量a被置为5,最终输出a=5,b=2。这个例子中,最关键的是分清defer传参与闭包捕获变量的实现机制。
func B(a int) int {
a++
return a
}
func A(a int) {
a++
fmt.Println(a)
}
func main() {
a := 1
defer A(B(a))
a++
fmt.Println(a)
}
这个例子中,main函数注册的defer函数是A,所以,defer链表项中_defer.fn存储的是A的funcval指针。但是deferproc执行时,需要保存A的参数到_defer结构体后面。这就需要在defer注册时拿到B的返回值。
既然B会在defer注册时执行,那么对B(a)求值时a=1。函数B的返回值就是2,也就是defer注册时保存的参数值为2,所以defer函数A执行时就会输出3。
这一次,我们抛开各种细节,只关注defer链表随着defer函数的注册与执行究竟会如何变化。
func A(){
//......
defer A1()
//......
defer A2()
//......
}
func A2(){
//......
defer B1()
defer B2()
//......
}
func A1(){
//......
}
//所有defer函数都正常执行......
这个例子中函数A注册两个defer,我们用函数名标记为A1和A2。
到函数A返回前执行deferreturn时,会判断defer链表头上的defer是不是A注册的。方法就是判断_defer结构体记录的sp是否等于A的栈指针.如果是A注册的,就保存defer函数调用的相关信息,然后把这一项从defer链表中移除,然后调用函数A2,A2执行时又注册两个defer,记为B1和B2。
函数A2返回前同样去执行defer链表,同样判断是否是自己注册的defer函数。所以B2执行,之后B1执行。此时A2仍然不知道自己注册的defer函数已经执行完了,直到下一个_defer.sp不等于A2的栈指针,A2注册的defer执行完,A2就可以结束了。
因为A1是函数A注册的defer函数,所以又回到A的defer执行流程。A1结束后,defer链表为空,函数A结束。
这个例子的关键是defer链表注册时添加链表项,执行时移除链表项的用法。
defer1.12的性能问题主要缘于两个方面:
Go1.13中defer性能的优化点,主要集中在减少defer结构体堆分配。我们通过一个例子,看看它是怎样做到的。
func A() {
defer B(10)
// code to do something
}
func B(i int) {
//......
}
像这样一段代码,在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()
}
注意上面的结构体d,它由两部分组成,一个是runtime._defer
结构体,一个是传给defer函数B的参数。它们被定义为函数A的局部变量,执行阶段会分配在函数栈帧的局部变量区域。接下来的runtime.deferprocStack
则会把栈上分配的_defer结构体注册到defer链表。通过这样的方式避免在堆上分配_defer结构体。
值得注意的是,1.13版本中并不是所有defer都能够在栈上分配。循环中的defer,无论是显示的for循环,还是goto形成的隐式循环,都只能使用1.12版本中的处理方式在堆上分配。即使只执行一次的for循环也是一样。
//显示循环
for i:=0; i< n; i++{
defer B(i)
}
......
//隐式循环
again:
defer B()
if i<n {
n++
goto again
}
所以Go1.13中,runtime._defer结构体增加了一个字段heap,用于标识是否为堆分配。
type _defer struct {
siz int32
started bool
heap bool //标识是否为堆分配
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}
defer函数的执行在1.13中没有变化,依然通过deferreturn实现,依然需要把_defer结构体后面的参数与返回值空间,拷贝到defer函数的调用者栈上。只不过不是从堆上拷贝到栈上,而是从栈上的局部变量空间拷贝到参数空间。
1.13版本的defer减少了_defer结构体的堆分配,但是仍然要使用defer链表。官方提供的性能优化在30%左右。
减少_defer结构体的堆分配,也是1.14版本中defer性能优化要持续践行的策略。但是具体做法与1.13版本不同。
func A(i int) {
defer A1(i, 2*i)
if(i > 1){
defer A2("Hello", "eggo")
}
// code to do something
return
}
func A1(a,b int){
//......
}
func A2(m,n string){
//......
}
上面这个例子中,函数A注册两个defer函数A1和A2,不过函数A2要到执行阶段根据条件判断是否要执行。先看defer函数A1这部分编译后的伪指令,Go1.14中会把A1需要的参数定义为局部变量,并在函数返回前直接调用A1。
func A(i int){
var a, b int = i, 2*i
//......
A1(a, b)
return
//......
}
通过这样的方式不仅不用构建_defer结构体,也用不到defer链表,但是到defer函数A2这里就行不通了。因为A2不一定要被执行,这要在执行阶段根据参数i的值来决定。
Go1.14通过增加一个标识变量df来解决这类问题。用df中的每一位对应标识当前函数中的一个defer函数是否要执行。
例如,函数A1要被执行,所以就通过df |= 1把df第一位置为1;在函数返回前再通过df&1判断是否要调用函数A1。
func A(i int){
var df byte
var a, b int = i, 2*i
df |= 1
//......
//code to do something
if df&1 > 0 {
df = df&^1
A1(a, b)
}
return
//......
}
所以像A2这样有条件执行的defer函数就可以像下面这样处理了。根据条件判断是否要把对应标识位置为1,函数返回前同样要根据标识符来判断是否要调用。
func A(i int){
var df byte
//A1的参数
var a, b int = i, 2*i
df |= 1
//A2的参数
var m,n string = "Hello", "eggo"
if i > 1 {
df |= 2
}
//code to do something
//判断A2是否要调用
if df&2 > 0 {
df = df&^2
A2(m, n)
}
//判断A1是否要调用
if df&1 > 0 {
df = df&^1
A1(a, b)
}
return
//省略部分与recover相关的逻辑
}
Go1.14把defer函数在当前函数内展开并直接调用,这种方式被称为open coded defer。这种方式不仅不用创建_defer结构体,也脱离了defer链表的束缚。不过这种方式依然不适用于循环中的defer,所以1.12版本defer的处理方式是一直保留的。
注意:
但是,必须要强调的是,我们一直在梳理的都是程序正常执行时defer的处理逻辑。一旦发生panic或者调用了runtime.Goexit函数,在这之后的正常逻辑就都不会执行了,而是直接去执行defer链表。那些使用open coded defer在函数内展开,因而没有被注册到链表的defer函数要通过栈扫描的方式来发现。
Go1.14中runtime._defer结构体又增加了几个字段:
type _defer struct {
siz int32
started bool
heap bool
openDefer bool //1
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
fd unsafe.Pointer //2
varp uintptr //3
framepc uintptr
借助这些信息,panic处理流程可以通过栈扫描的方式找到这些没有被注册到defer链表的defer函数,并按照正确的顺序执行。
所以,实际上Go1.14版本中defer的确变快了,但panic变得更慢了…
参考资料:
https://mp.weixin.qq.com/s/gaC2gmFhJezH-9-uxpz07w