文章目录
GoLang之defer底层系列一(Go1.12 )
1.defer编译伪指令
关于defer,我们知道它会在函数返回之前倒叙执行
像这样的代码,编译后的伪指令是这样的,defer指令对应到两部分内容:
1.deferproc负责把要执行的函数保存起来,我们称之为defer注册,deferproc函数会返回0,“if r >0{go to ret}”和panic recover有关,先忽略,对应要跳转"ret :{runtime.deferreturn}"的也先忽略,下载defer正常执行的逻辑就很清晰了;
2.defer注册完成(deferproc)后程序会继续执行后面的逻辑(code to do something),直到返回之前通过deferreturn执行注册的defer函数。正是因为先注册后调用,才实现了defer延迟执行的效果
2.分析倒叙执行
defer信息会注册到一个链表,而当前执行的goroutine持有这个链表的头指针,每个goroutine在运行时都有一个对应的结构体g,其中有一个字段指向defer链表头,defer链表链起来的是一个个_defer结构体,新注册的defer会添加到链表头,执行时也是从头开始,所以defer才会表现为倒序执行
3._defer结构体
我们先把_defer结构体展开来看一下:
1.siz记录defer参数与返回值共占多少字节,这段空间会直接分配在_defer结构体后面,用于在注册时保存参数,并在执行时拷贝到调用者参数与返回值空间;
2.started字段标记defer是否已经执行;
3.sp记录的是注册这个defer的函数栈指针,通过它函数可以判断自己注册的defer是否已经执行完了;
4.pc是deferproc的返回地址
5.fn是要注册的Function Value
6.link是要链接到其前一个注册的_defer结构体
4.deferproc函数调用栈变化(无捕获列表)
func A(a int) {
fmt.Println(a)
}
func main() {
a, b := 1, 2
defer A(a)
a = a + b
fmt.Println(a, b)
/**
输出:3 2
1
*/
}
这里函数A注册了一个defer函数A1,A的栈帧首先是两个局部变量(a=1,b=2)
然后就要注册defer函数A1了:
deferproc函数原型只有两个参数,第一个参数是defer函数A1的参数加返回值共占用多大空间,A1没有返回值,只有一个int参数,所以第一个参数为8(64位下要占8字节);
第二个参数是一个function value,之前说过,没有捕获列表的function value 在编译阶段会做出优化,就是在只读数据段分配一个共用的funcval结构体,"fn=addr1"是指向函数A1指令入口的funcval,所以deferproc的第二个参数就是它的地址。
deferproc函数调用时,编译器会在它自己的两个参数后面,开辟一段空间,用于存放dfefer函数的返回值和参数,这一段空间会被直接拷贝到_defer结构体的后面
A1只有1个参数,放在deferproc函数自己的两个参数后面
deferproc函数执行时,需要堆分配一段空间,用于存放_defer结构体,以及后面siz大小的参数与返回值;
1.第一个参数加返回值共占8字节然后;
2.started=fasle;
3.调用者栈指针指向SP OF A;
4.返回地址是被addr;
5.被注册的function value为addr2
defer结构体后面的者8字节就用来存放传给A1的参数a =1;
然后这个结构体就被添加到defer链表头,deferproc注册结束
执行到a=a+b后局部变量被置为3
下一步输出局部变量a=3,b=2
然后就到deferreturn执行链表了,从当前goroutine拿到链表头上的这个_defer结构体,通过fn找到funcval,拿到函数入口地址,调用A1时会把_defer后面的参数与返回值,整个拷贝到A1的调用者栈上,然后A1开始执行
这里输出参数值1,这里的关键是“defer函数的参数在注册时拷贝到堆上执行时又拷贝到栈上”
5.deferpool
实际上go语言会预分配不同规格的defer池,执行时从空闲_defer中取一个来用,没有空闲或者没有大小合适的再进行堆分配,用完以后再放回空闲_defer池,这样可以避免频繁的堆分配和回收
6.4deferproc函数调用栈变化(有捕获列表)
func main() {
a, b := 1, 2
defer func(b int) () {
a = a + b
fmt.Println(a, b)
}(b)
a = a + b
fmt.Println(a, b)
/*输出:
3 2
5 2
*/
}
既然deferproc注册的是一个function value,那就来看看有捕获列表时是什么情况,这个例子中defer函数不止要传递局部变量b做参数,还捕获了外层函数的局部变量a,形成闭包;
假如这个函数的闭包指令在"A_func1"那里,执行阶段会使用addr1这个入口地址创建闭包对象
由于局部变量a除了初始化过,还被修改过,所以局部变量改为堆分配,栈上存储它的地址;
还有个局部变量b=2
然后创建闭包对象,堆分配一个funcval结构体,捕获列表中存储a的地址
deferproc执行时,defer结构体中的fn保存的就是这个funcval结构体的起始地址,除此之外还要拷贝参数b的值到后面,然后把这个defer结构体添加到defer链表头,deferproc结束
执行到a=a+3这一步,a被置为3(注意:a在堆那里);
下一步a被输出3,b被输出2
然后就到deferreturn了,从defer链表头拿到这个defer结构体。执行注册的defer函数时,把参数b拷贝到栈上的参数空间
闭包函数需要开始找捕获列表,通过寄存器存储的funcval地址加上偏移,找到捕获变量a的地址;
即执行到a=a+b时,a=3,b=2,a+b=5,所以下一步输出a=5,b=2;
这里最关键的是分清defer传参与闭包捕获变量的实现机制
7.deferproc函数调用栈变化(简单函数嵌套)
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)
/*输出:
2
3
*/
}
现在就能解释形如defer A(B(a))这样的问题了。这里defer注册的函数是A,defer链表存储的也是A的funcval指针。因为注册时需要保存A的参数,就要拿到B的返回值,所以B(a)会在defer注册时执行。注册时a=1,defer注册保存的参数值就是2,所以defer执行时函数A会输出3
8.defer链表变化(复杂函数嵌套)
最后来看一个defer嵌套的例子,这一个抛开所有细节。只看defer链表随着A的执行会怎样变化,首先函数A注册两个defer,到A返回前执行deferreturn时,会判断defer链表头上的defer是不是A注册的,方法就是判断defer结构体记录的sp是否等于A的栈指针
保存函数调用的相关信息后,把它从defer链表中移除,然后执行函数A2,又注册两个defer(B1与B2),A2返回前同样去执行defer链表,同样判断是否是自己注册的defer函数
然后B2执行,同样的流程B1执行
此时A2仍然不知道自己注册的defer已经执行完了,直到下一个_defer.SP不等于自己的栈指针,然后A2就可以结束了
再次回到A的defer执行流程,执行A1,A1结束后defer链表为空,函数A结束。这里的关键是了解defer链表注册时添加链表项,执行时移除链表项的用法
Go1.12版本的defer基本设计思路就算梳理完了,这一版本的defer比较明显的问题就是慢。
第一个原因是_defer结构体堆分配,即使有预分配的deferpool,也需要去堆上获取与释放,而且参数还要再堆栈上来回拷贝。
第二个原因是使用链表注册defer信息,而链表本身操作比较慢。所以Go1.13和1.14中分别做了不同的优化