Go defer 原理剖析篇 — 深度解密 defer 原理,究竟背着程序猿做了多少事情

本文深入剖析Go语言中的defer关键字,详细解释了编译器如何处理defer,包括数据结构、内存分配、参数传递、多defer处理及返回值顺序。文章揭示了defer背后的关键细节,如预计算参数、栈上和堆上分配,并讨论了性能影响因素。
摘要由CSDN通过智能技术生成

更多干货:关注公众号 奇伢云存储

上一次从使用姿势和特性上分析了 defer 关键字,让我们对此有个形象的概念,然后剖析了函数调用的本质原理,接下来剖析就是真正 defer 这个关键字背后的原理了。

思考几个问题:

  1. 编译器怎么编译 defer 关键字?
  2. defer 语句怎么传递参数?
  3. 一个函数内多个 defer 语句的时候,会发生什么?
  4. 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 关键字的时候,添加了一些“用户不可见”的函数:

  1. deferprocStack
  2. deferreturn

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

go 1.13 release note

This release improves performance of most uses of defer by 30%.

go 1.13 之前只有 defer 语句只会被编译器翻译成两个过程:

  1. 回调注册函数过程:deferproc
  2. 执行回调函数链过程:deferreturn

go 1.13 带来的 deferprocStack 函数,这个函数就是这个 30% 性能提升的核心手段。deferprocStackdeferproc 的目的都是注册回调函数,这个还是不变,但是不同的是 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.Escast.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的时候,才会设置成 EscNevere.loopdepth 字段是用于检测嵌套循环作用域的,换句话说,defer 如果在嵌套作用域的上下文中,那么就可能导致 struct _defer 分配在堆上,如下:

package main

func main() {
   
	for i := 0; i < 2; i++ {
   
		defer func() {
   
			_ = i
		}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值