文章目录
更多干货:关注公众号 奇伢云存储
上一次从使用姿势和特性上分析了 defer 关键字,让我们对此有个形象的概念,然后剖析了函数调用的本质原理,接下来剖析就是真正 defer 这个关键字背后的原理了。
思考几个问题:
- 编译器怎么编译 defer 关键字?
- defer 语句怎么传递参数?
- 一个函数内多个 defer 语句的时候,会发生什么?
- defer 和 return 返回值运行顺序究竟是怎样的?
编译器怎么编译 defer
defer dosomething(x)
通俗来讲,执行 defer 语句之后,是注册记录一个稍后执行的函数。把函数名和参数确定下来,不会立即调用,而是等到从当前函数 return
出去的时候。
如果要知道 defer 做了什么,就要我们先从最基本的数据结构和内部函数讲起。
struct _defer
数据结构
提示:该数据结构为 go 1.13 版本,go 1.14 版本的比这个稍微复杂,加了一些开放编码优化需要的字段。
type _defer struct {
siz int32 // 参数和返回值的内存大小
started bool
heap bool // 区分该结构是在栈上分配的,还是对上分配的
sp uintptr // sp 计数器值,栈指针;
pc uintptr // pc 计数器值,程序计数器;
fn *funcval // defer 传入的函数地址,也就是延后执行的函数;
_panic *_panic // panic that is running defer
link *_defer // 链表
}
每一次的 defer 调用都会对应到一个 _defer
结构体,一个函数内可以有多个 defer 调用,所以自然需要一个数据结构来组织这些 _defer
结构体。_defer
按照对齐规则占用 48 字节的内存。在 _defer
结构体中的 link
字段,这个字段把所有的 _defer
串成一个链表,表头是挂在 Goroutine 的 _defer
字段。效果如下:
还有一个重点,_defer
结构只是一个 header ,结构紧跟的是延迟函数的参数和返回值的空间,大小由 _defer.siz
指定。这块内存的值在 defer 关键字执行的时候填充好。这里引出一个下面重点的概念:延迟函数的参数是预计算的。
struct _defer
内存分配
以此为例:
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 build -gclfags "-N -l"
我们看下编译成的二进制代码:
从汇编指令我们看到,编译器在遇到 defer
关键字的时候,添加了一些“用户不可见”的函数:
deferprocStack
deferreturn
go 1.13 正式版本的发布提升了 defer 的性能,号称针对 defer 场景提升了 30% 的性能。
This release improves performance of most uses of defer by 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 < 2; i++ {
defer func() {
_ = i
}