GoLang之defer底层系列一(v 1.12)

GoLang之defer底层系列一(v 1.12)

注:本文以Go SDK v1.12进行讲解

1.前言

“ defer如何延迟,因何倒序?”
“ defer函数参数怎么传递?”
“ 加个闭包再多套几层,你还hold住吗?”
“ 都说defer1.12性能有坑,那坑从何来?又该怎么填?”
如何延迟?因何倒序?

2.如何延迟?因何倒序?

2.1如何延迟?

Go语言的defer是一个很方便的机制,能够把某些函数调用推迟到当前函数返回前才实际执行。我们可以很方便的用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“注册”。
从函数原型来看,deferproc函数有两个参数,第一个是被注册的defer函数的参数加返回值共占多少字节;第二个参数是一个runtime.funcval结构体的指针,也就是一个Function Value,已介绍过,此不赘述。

func deferproc(siz int32, fn *funcval)

与defer指令相关的第二部分就是deferreturn,它被编译器插入到函数返回以前调用,负责执行已经注册的defer函数。所以defer函数之所以能延迟到函数返回前执行,就是因为先注册,后调用。

2.2因何倒序?

再来看看defer函数为什么会倒序执行。
defer注册信息会保存到defer链表。每个goroutine在运行时都对应一个runtime.g结构体,其中有一个_defer字段,保存的就是defer链表的头指针。

在这里插入图片描述

deferproc新注册的defr信息会添加到链表头。deferreturn执行时也从链表头开始,所以defer才会表现为倒序执行。

在这里插入图片描述

理解了这些,就可以继续细化,看看defer注册时保存了什么信息,defer链表中每个元素究竟是什么结构了。

3.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
 }

1.siz : 由deferproc第一个参数传入,就是defer函数参数加返回值的总大小。
这段空间会直接分配在_defer结构体后面,用于在注册时保存给defer函数传入的参数,并在执行时直接拷贝到defer函数的调用者栈上。
2.started : 标识defer函数是否已经开始执行;
3.sp : 就是注册defer函数的函数栈指针;
4.pc : 是deferproc函数返回后要继续执行的指令地址;
5.fn :由deferproc的第二个参数传入,也就是被注册的defer函数;
6._panic : 是触发defer函数执行的panic指针,正常流程执行defer时它就是nil;
7.link: 自然是链到之前注册的那个_defer结构体。

这一篇我们只关注正常流程下defer函数的执行,不考虑panic或runtime.Goexit()的情况。

4.defer传参机制

func A1(a int) {
	fmt.Println(a)
}
func A() {
	a, b := 1, 2
	defer A1(a)

	a = a + b
	fmt.Println(a, b)
}
func main() {
	A()
	/*
	  3  2
		1
	*/
}

 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有关,且先忽略。

在这里插入图片描述

deferproc函数执行,需要堆分配一段空间,用于放_defer结构体以及后面siz大小的参数与返回值。_defer结构体的第一个字段,A1的参数加返回值共占8字节;defer函数尚未执行,所以started=false;sp就是调用者A的栈指针;pc就是deferproc函数的返回地址return addr;被注册的function value为A1;defer结构体后面的8字节用来保存传递给A1的参数。

在这里插入图片描述

然后这个_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函数的参数在注册时拷贝到堆上,执行时再拷贝到栈上。

5.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)
}
func main() {
	A()
	/*
		3 2
		5 2
	*/
}
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传参与闭包捕获变量的实现机制。

6.defer( A( B© ) )


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
	*/
}

这个例子中,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。

7.defer嵌套

这一次,我们抛开各种细节,只关注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链表注册时添加链表项,执行时移除链表项的用法。

“理解了defer注册与执行的逻辑,再配合之前介绍过的Function Value、函数调用栈等内容,就很容易理解上面几个例子,也就不用刷那些重复的defer面试题了。”

8.defer1.12,有点儿慢

因为defer真的很方便,所以大家都已经习惯了随手使用它。但是与一般的函数调用比起来,defer1.12的实现方式会在调用时造成较大的额外开销,尤其是在锁释放这种场景。因此经常被一些库设计者所诟病,甚至有些项目的注释中写明了不用defer能节省多少多少纳秒。

defer1.12的性能问题主要缘于两个方面:
1._defer结构体堆分配,即使有预分配的deferpool,也需要去堆上获取与释放。而且defer函数的参数还要在注册时从栈拷贝到堆,执行时又要从堆拷贝到栈。
2.defer信息保存到链表,而链表操作比较慢。

但是,defer作为一个关键的语言特性,怎能如此受人诟病?所以GO语言在1.13和1.14中做出了不同的优化。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

GoGo在努力

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

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

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

打赏作者

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

抵扣说明:

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

余额充值