Go defer 原理和源码剖析

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<
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值