GoLang之defer底层系列一(Go1.12)

GoLang之defer底层系列一(Go1.12 )

1.defer编译伪指令

关于defer,我们知道它会在函数返回之前倒叙执行

image-20220309125916257

像这样的代码,编译后的伪指令是这样的,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延迟执行的效果

image-20220309130059114

image-20220309130432752

2.分析倒叙执行

defer信息会注册到一个链表,而当前执行的goroutine持有这个链表的头指针,每个goroutine在运行时都有一个对应的结构体g,其中有一个字段指向defer链表头,defer链表链起来的是一个个_defer结构体,新注册的defer会添加到链表头,执行时也是从头开始,所以defer才会表现为倒序执行

image-20220309130848551

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)

image-20220309131314162

然后就要注册defer函数A1了:

deferproc函数原型只有两个参数,第一个参数是defer函数A1的参数加返回值共占用多大空间,A1没有返回值,只有一个int参数,所以第一个参数为8(64位下要占8字节);

第二个参数是一个function value,之前说过,没有捕获列表的function value 在编译阶段会做出优化,就是在只读数据段分配一个共用的funcval结构体,"fn=addr1"是指向函数A1指令入口的funcval,所以deferproc的第二个参数就是它的地址。

image-20220309131530688

image-20220309131805967

deferproc函数调用时,编译器会在它自己的两个参数后面,开辟一段空间,用于存放dfefer函数的返回值和参数,这一段空间会被直接拷贝到_defer结构体的后面

image-20220309132543320

A1只有1个参数,放在deferproc函数自己的两个参数后面

image-20220309132735599

image-20220309132825347

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注册结束

image-20220309133501483

image-20220309133449613

image-20220309133434995

执行到a=a+b后局部变量被置为3

image-20220309134015307

下一步输出局部变量a=3,b=2

image-20220309134046320

然后就到deferreturn执行链表了,从当前goroutine拿到链表头上的这个_defer结构体,通过fn找到funcval,拿到函数入口地址,调用A1时会把_defer后面的参数与返回值,整个拷贝到A1的调用者栈上,然后A1开始执行

image-20220309134352388

这里输出参数值1,这里的关键是“defer函数的参数在注册时拷贝到堆上执行时又拷贝到栈上”

image-20220309134456203

5.deferpool

实际上go语言会预分配不同规格的defer池,执行时从空闲_defer中取一个来用,没有空闲或者没有大小合适的再进行堆分配,用完以后再放回空闲_defer池,这样可以避免频繁的堆分配和回收

image-20220309133722337

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这个入口地址创建闭包对象

image-20220309135649413

由于局部变量a除了初始化过,还被修改过,所以局部变量改为堆分配,栈上存储它的地址;

还有个局部变量b=2

image-20220309135855817

然后创建闭包对象,堆分配一个funcval结构体,捕获列表中存储a的地址

image-20220309140012314

deferproc执行时,defer结构体中的fn保存的就是这个funcval结构体的起始地址,除此之外还要拷贝参数b的值到后面,然后把这个defer结构体添加到defer链表头,deferproc结束

image-20220309140104074

执行到a=a+3这一步,a被置为3(注意:a在堆那里);

下一步a被输出3,b被输出2

image-20220309140300864

然后就到deferreturn了,从defer链表头拿到这个defer结构体。执行注册的defer函数时,把参数b拷贝到栈上的参数空间

image-20220309140408015

闭包函数需要开始找捕获列表,通过寄存器存储的funcval地址加上偏移,找到捕获变量a的地址;

即执行到a=a+b时,a=3,b=2,a+b=5,所以下一步输出a=5,b=2;

这里最关键的是分清defer传参与闭包捕获变量的实现机制

image-20220309140721901

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

image-20220309141648227

8.defer链表变化(复杂函数嵌套)

最后来看一个defer嵌套的例子,这一个抛开所有细节。只看defer链表随着A的执行会怎样变化,首先函数A注册两个defer,到A返回前执行deferreturn时,会判断defer链表头上的defer是不是A注册的,方法就是判断defer结构体记录的sp是否等于A的栈指针

image-20220309141759165

保存函数调用的相关信息后,把它从defer链表中移除,然后执行函数A2,又注册两个defer(B1与B2),A2返回前同样去执行defer链表,同样判断是否是自己注册的defer函数

image-20220309142027855

然后B2执行,同样的流程B1执行

image-20220309142152214

image-20220309142211315

此时A2仍然不知道自己注册的defer已经执行完了,直到下一个_defer.SP不等于自己的栈指针,然后A2就可以结束了

image-20220309142302996

再次回到A的defer执行流程,执行A1,A1结束后defer链表为空,函数A结束。这里的关键是了解defer链表注册时添加链表项,执行时移除链表项的用法

image-20220309142334903

Go1.12版本的defer基本设计思路就算梳理完了,这一版本的defer比较明显的问题就是慢。
第一个原因是_defer结构体堆分配,即使有预分配的deferpool,也需要去堆上获取与释放,而且参数还要再堆栈上来回拷贝。
第二个原因是使用链表注册defer信息,而链表本身操作比较慢。所以Go1.13和1.14中分别做了不同的优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoGo在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值