关于golang:Go-defer-原理和源码剖析

49次阅读

共计 7946 个字符,预计需要花费 20 分钟才能阅读完成。

Go 语言中有一个十分有用的保留字 defer,defer 语句能够调用一个函数,该函数的执行被推延到包裹它的函数返回时执行。

defer 语句调用的函数,要么是因为包裹它的函数执行了 return 语句,达到了函数体的末端,要么是因为对应的 goroutine 产生了 panic。

在理论的 go 语言程序中,defer 语句能够代替其它语言中 try…catch… 的作用,也能够用来解决开释资源等收尾操作,比方敞开文件句柄、敞开数据库连贯等。

1. 编译器编译 defer 过程

defer dosomething(x)

简略来说,执行 defer 语句,实际上是注册了一个稍后执行的函数,确定了函数名和参数,但不会立刻调用,而是把调用过程推延到以后函数 return 或者产生 panic 的时候。

咱们先理解一下 defer 相干的数据结构。

1) struct _defer 数据结构
go 语言程序中每一次调用 defer 都生成一个 _defer 构造体。

type _defer struct {
    siz     int32 // 参数和返回值的内存大小
    started boul
    heap    boul    // 辨别该构造是在栈上调配的,还是对上调配的
    sp        uintptr  // sp 计数器值,栈指针;pc        uintptr  // pc 计数器值,程序计数器;fn        *funcval // defer 传入的函数地址,也就是延后执行的函数;
    _panic    *_panic  // panic that is running defer
    link      *_defer   // 链表
}

咱们默认应用了 go 1.13 版本的源代码,其它版本相似。

一个函数内能够有多个 defer 调用,所以天然须要一个数据结构来组织这些 _defer 构造体。_defer 依照对齐规定占用 48 字节的内存。在 _defer 构造体中的 link 字段,这个字段把所有的 _defer 串成一个链表,表头是挂在 Goroutine 的 _defer 字段。

_defer 的链式构造如下:

_defer.siz 用于指定提早函数的参数和返回值的空间,大小由 _defer.siz 指定,这块内存的值在 defer 关键字执行的时候填充好。

defer 提早函数的参数是预计算的,在栈上调配空间。每一个 defer 调用在栈上调配的内存布局如下图所示:

其中 _defer 是一个指针,指向一个 struct _defer 对象,它可能调配在栈上,也可能调配在堆上。

2) struct _defer 内存调配
以下是一个应用 defer 的范例,文件名为 test_defer.go:

package main

func doDeferFunc(x int) {println(x)
}

func doSomething() int {
    var x = 1
    defer doDeferFunc(x)
    x += 2
    return x
}

func main() {x := doSomething()
    println(x)
}

编译以上代码,加上去除优化和内链选项:

go tool compile -N -l test_defer.go

导出汇编代码:

go tool objdump test_defer.o

咱们看下编译成的二进制代码:

从汇编指令咱们看到,编译器在遇到 defer 关键字的时候,增加了一些运行库函数:deferprocStack 和 deferreturn。

go 1.13 正式版本的公布晋升了 defer 的性能,号称针对 defer 场景晋升了 30% 的性能。

go 1.13 之前的版本 defer 语句会被编译器翻译成两个过程:回调注册函数过程:deferproc 和 deferreturn。

go 1.13 带来的 deferprocStack 函数,这个函数就是这个 30% 性能晋升的外围伎俩。deferprocStack 和 deferproc 的目标都是注册回调函数,然而不同的是 deferprocStatck 是在栈内存上调配 struct _defer 构造,而 deferproc 这个是须要去堆上调配构造内存的。而咱们绝大部分的场景都是能够是在栈上调配的,所以天然整体性能就晋升了。栈上分配内存天然是比对上要快太多了,只须要扭转 rsp 寄存器的值就能够进行调配。

那么什么时候调配在栈上,什么时候调配在堆上呢?

在编译器相干的文件(src/cmd/compile/internal/gc/ssa.go)里,有个条件判断:

func (s *state) stmt(n *Node) {
 
    case ODEFER:
        d := callDefer
        if n.Esc == EscNever {d = callDeferStack}
}

n.Esc 是 ast.Node 的逃逸剖析的后果,那么什么时候 n.Esc 会被置成 EscNever 呢?

这个在逃逸剖析的函数 esc 里(src/cmd/compile/internal/gc/esc.go):

func (e *EscState) esc(n *Node, parent *Node) {

    case ODEFER:
        if e.loopdepth == 1 { // top level
            n.Esc = EscNever // force stack allocation of defer record (see ssa.go)
            break
        }
}

这里 e.loopdepth 等于 1 的时候,才会设置成 EscNever,e.loopdepth 字段是用于检测嵌套循环作用域的,换句话说,defer 如果在嵌套作用域的上下文中,那么就可能导致 struct _defer 调配在堆上,如下:

package main

func main() {
    for i := 0; i < 10; i++ {defer func() {_ = i}()}
}

编译器生成的则是 deferproc:

当 defer 外层呈现显式(for)或者隐式(goto)的时候,将会导致 struct _defer 构造体调配在堆上,性能就会变差,这个编程的时候要留神。

编译器就能决定 _defer 构造体调配在栈上还是堆上,对应函数别离是 deferprocStatck 和 deferproc 函数,这两个函数都很简略,目标统一:调配出 struct _defer 的内存构造,把回调函数初始化进去,挂到链表中。

3) deferprocStack 栈上调配
deferprocStack 函数做了哪些事件呢?

// 进入这个函数之前,就曾经在栈上调配好了内存构造
func deferprocStack(d *_defer) {gp := getg()

    // siz 和 fn 在进入这个函数之前曾经赋值
    d.started = false
    // 表明是栈的内存
    d.heap = false
    // 获取到 caller 函数的 rsp 寄存器值,并赋值到 _defer 构造 sp 字段中
    d.sp = getcallersp()
    // 获取到 caller 函数的 rip 寄存器值,并赋值到 _defer 构造 pc 字段中
    // 依据函数调用的原理,咱们就晓得 caller 的压栈的 pc (rip) 值就是 deferprocStack 的下一条指令
    d.pc = getcallerpc()

    // 把这个 _defer 构造作为一个节点,挂到 goroutine 的链表中
    *(*uintptr)(unsafe.Pointer(&d._panic)) = 0
    *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
    *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
    // 留神,非凡的返回,不会触发提早调用的函数
    return0()}

小结:

  • 因为是栈上分配内存的,所以调用到 deferprocStack 之前,编译器就曾经把 struct _defer 构造的函数筹备好了;
  • _defer.heap 字段用来标识这个构造体调配在栈上;
    保留上下文,把 caller 函数的 rsp,pc(rip)寄存器的值保留到 _defer 构造体;
  • _defer 作为一个节点挂接到链表。留神:表头是 goroutine 构造的 _defer 字段,而在一个协程工作中大部分有屡次函数调用的,所以这个链表会挂接一个调用栈上的 _defer 构造,执行的时候依照 rsp 来过滤辨别;
  • deferprocStack 栈上调配

4) deferproc 堆上调配
堆上调配的函数为 deferproc,简化逻辑如下:

func deferproc(siz int32, fn *funcval) {
  // arguments of fn fullow fn
    // 获取 caller 函数的 rsp 寄存器值
    sp := getcallersp()
    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    // 获取 caller 函数的 pc(rip)寄存器值
    callerpc := getcallerpc()

    // 调配 struct _defer 内存构造
    d := newdefer(siz)
    if d._panic != nil {throw("deferproc: d.panic != nil after newdefer")
    }
    // _defer 构造体初始化
    d.fn = fn
    d.pc = callerpc
    d.sp = sp
    switch siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
    default:
        memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
    }
    // 留神,非凡的返回,不会触发提早调用的函数
    return0()}

小结:

  • 与栈上调配不同,struct _defer 构造是在该函数里调配的,调用 newdefer 调配构造体,newdefer 函数则是先去 pool 缓存池里看一眼,有就间接取用,没有就调用 mallocgc 从堆上分配内存;
  • deferproc 承受入参 siz,fn,这两个参数别离标识提早函数的参数和返回值的内存大小,提早函数地址;
  • _defer.heap 字段用来标识这个构造体调配在堆上;
  • 保留上下文,把 caller 函数的 rsp,pc(rip)寄存器的值保留到 _defer 构造体;
  • _defer 作为一个节点挂接到链表;

5) 执行 defer 函数链

编译器遇到 defer 语句,会插入两个函数:

调配函数:deferproc 或者 deferprocStack;
执行函数:deferreturn。
包裹 defer 语句的函数退出的时候,由 deferreturn 负责执行所有的提早调用链。

func deferreturn(arg0 uintptr) {gp := getg()
    // 获取到最前的 _defer 节点
    d := gp._defer
    // 函数递归终止条件(d 链表遍历实现)if d == nil {return}
    // 获取 caller 函数的 rsp 寄存器值
    sp := getcallersp()
    if d.sp != sp {
        // 如果 _defer.sp 和 caller 的 sp 值不统一,那么间接返回;// 因为,就阐明这个 _defer 构造不是在该 caller 函数注册的  
        return
    }

    switch d.siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
    default:
        memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
    }
    // 获取到提早回调函数地址
    fn := d.fn
    d.fn = nil
    // 把以后 _defer 节点从链表中摘除
    gp._defer = d.link
    // 开释 _defer 内存(次要是堆上才会须要解决,栈上的随着函数执行完,栈膨胀就回收了)freedefer(d)
    // 执行提早回调函数
    jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

代码阐明:

  • 遍历 defer 链表,一个个执行,程序链表从前往后执行,执行一个摘除一个,直到链表为空;
  • jmpdefer 负责跳转到提早回调函数执行指令,执行完结之后,跳转回 deferreturn 里执行;
  • _defer.sp 的值能够用来判断哪些是以后 caller 函数注册的,这样就能保障只执行本人函数注册的提早回调函数;
  • 例如,a() -> b() -> c(),a 调用 b,b 调用 c,而 a,b,c 三个函数都有 defer 注册提早函数,那么天然是 c()函数返回的时候,执行 c 的回调;

2. defer 传递参数

1) 预计算参数

_defer 在栈上作为一个 header,提早回调函数(defer)的参数和返回值紧接着 _defer 搁置,而这个参数值是在 defer 执行的时候就设置好了,也就是预计算参数,而非等到执行 defer 函数的时候才去获取。

举个例子,执行 defer func(x, y) 的时候,x,y 这两个实参是计算的进去的,Go 中的函数调用都是值传递。那么就会把 x,y 的值拷贝到 _defer 构造体之后。再看个例子:

package main

func main() {
    var x = 1
    defer println(x)
    x += 2
    return

}
这个程序输入是什么呢?是 1,还是 3?答案是:1。defer 执行的函数是 println,println 参数是 x,x 的值传进去的值则是在 defer 语句执行的时候就确认了的。

2) defer 的参数筹备

defer 提早函数执行的参数曾经保留在和 _defer 一起的间断内存块了。那么执行 defer 函数的时候,参数是哪里来呢?当然不是间接去 _defer 的地址找。因为这里是走的规范的函数调用。

在 Go 语言中,一个函数的参数由 caller 函数筹备好,比如说,一个 main() -> A(7) -> B(a) 造成相似以下的栈帧:

所以,deferreturn 除了跳转到 defer 函数指令,还须要做一个事件:把 defer 提早回调函数须要的参数筹备好(空间和值)。那么就是如下代码来做的眼帘:

func deferreturn(arg0 uintptr) {

    switch d.siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
    default:
        memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
    }

}

arg0 就是 caller 用来搁置 defer 参数和返回值的栈地址。这段代码的意思就是,把 _defer 事后的筹备好的参数,copy 到 caller 栈帧的某个地址(arg0)。

3. 执行多条 defer

后面曾经具体阐明了,_defer 是一个链表,表头是 goroutine._defer 构造。一个协程的函数注册的是挂同一个链表,执行的时候依照 rsp 来辨别函数。并且,这个链表是把新元素插在表头,而执行的时候是从返回后执行,所以这里导致了一个 LIFO 的个性,也就是先注册的 defer 函数后执行。

4. defer 和 return 运行程序

蕴含 defer 语句的函数返回时,先设置返回值还是先执行 defer 函数?

1) 函数的调用过程
要了解这个过程,首先要晓得函数调用的过程:

go 的一行函数调用语句其实非原子操作,对应多行汇编指令,包含 1)参数设置,2)call 指令执行;
其中 call 汇编指令的内容也有两个:返回地址压栈(会导致 rsp 值往下增长,rsp-0x8),callee 函数地址加载到 pc 寄存器;
go 的一行函数返回 return 语句其实也非原子操作,对应多行汇编指令,包含 1)返回值设置 和 2)ret 指令执行;
其中 ret 汇编指令的内容是两个,指令 pc 寄存器复原为 rsp 栈顶保留的地址,rsp 往上缩减,rsp+0x8;
参数设置在 caller 函数里,返回值设置在 callee 函数里;
rsp, rbp 两个寄存器是栈帧的最重要的两个寄存器,这两个值划定了栈帧;
最重要的一点:Go 的 return 的语句调用是个复合操作,能够对应一下两个操作序列:

    1. 设置返回值
    1. ret 指令跳转到 caller 函数

2) return 之后是先返回值还是先执行 defer 函数?
Golang 官网文档有明确阐明:

That is, if the surrounding function returns through an explicit return statement, deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller.

也就是说,defer 的函数链调用是在设置了返回值之后,然而在运行指令上下文返回到 caller 函数之前。

所以含有 defer 注册的函数,执行 return 语句之后,对应执行三个操作序列:

    1. 设置返回值
    1. 执行 defer 链表
    1. ret 指令跳转到 caller 函数
      那么,依据这个原理咱们来解析如下的行为:
func f1 () (r int) {
    t := 1
    defer func() {t = t + 5}()
    return t
}

func f2() (r int) {defer func(r int) {r = r + 5}(r)
    return 1
}

func f3() (r int) {defer func () {r = r + 5} ()
    return 1
}

这三个函数的返回值别离是多少?

答案:f1() -> 1,f2() -> 1,f3() -> 6。

a) 函数 f1 执行 return t 语句之后:

设置返回值 r = t,这个时候局部变量 t 的值等于 1,所以 r = 1;
执行 defer 函数,t = t+5,之后局部变量 t 的值为 6;
执行汇编 ret 指令,跳转到 caller 函数;
所以,f1() 的返回值是 1;

b) 函数 f2 执行 return 1 语句之后:

设置返回值 r = 1;
执行 defer 函数,defer 函数传入的参数是 r,r 在预计算参数的时候值为 0,Go 传参为值传递,0 赋值给了匿名函数的参数变量,所以,r = r+5,匿名函数的参数变量 r 的值为 5;
执行汇编 ret 指令,跳转到 caller 函数;
所以,f2() 的返回值还是 1;

c) 函数 f3 执行 return 1 语句之后:

设置返回值 r = 1;
执行 defer 函数,r = r+5,之后返回值变量 r 的值为 6(这是个闭包函数,留神和 f2 辨别);
执行汇编 ret 指令,跳转到 caller 函数;
所以,f1() 的返回值是 6;

5. 总结

  • defer 关键字执行对应 _defer 数据结构,在 go1.1 – go1.12 期间始终是堆上调配,在 go1.13 之后优化成栈上调配 _defer 构造,性能晋升显著;
  • _defer 大部分场景是调配在栈上的,然而遇到循环嵌套的场景会调配到堆上,所以编程时要留神 defer 应用场景,否则可能出性能问题;
  • _defer 对应一个注册的提早回调函数(defer),defer 函数的参数和返回值紧跟 – – – _defer,能够了解成 header,_defer 和函数参数,返回值所在内存是一块间断的空间,其中 _defer.siz 指明参数和返回值的所占空间大小;
  • 同一个协程里 defer 注册的函数,都挂在一个链表中,表头为 goroutine._defer;
  • 新元素插入在最后面,遍历执行的时候则是从返回后执行。所以 defer 注册函数具备 LIFO 的个性,也就是后注册的先执行;不同的函数都在这个链表上,以 _defer.sp 辨别;
  • defer 的参数是预计算的,也就是在 defer 关键字执行的时候,参数就确认,赋值在 _defer 的内存块前面。执行的时候,copy 到栈帧对应的地位上;
  • return 对应 3 个动作的复合操作:设置返回值、执行 defer 函数链表、ret 指令跳转。

参考资料:
go 语言教程
Go 语言深刻分析

正文完
 0