【defer】全新升级1.14以后的defer|Go主题月

1,375 阅读2分钟

1.14版本中的defer

go1.14中,官方又对defer做了升级,据说这次升级把速度提升了一个量级。

defer_open

在编译期间,会直接把defer放到函数末尾去执行,省去了_defer结构体和链表的使用。官方把这种方法命名为:开放编码(Open Coded)

不过需要满足以下条件,否则并不会使用开放编码:

  • 函数的 defer 数量少于或者等于 8 个;
  • 函数的 defer 关键字不能在循环中执行;
  • 函数的 return 语句与 defer 语句的乘积小于或者等于 15 个;

延迟比特

为什么上面说defer的数量要小于等于8个呢?这是由于延迟比特的限制。延迟比特只有8个,默认值为0,每个对应一个defer,延迟比特的作用是判断defer语句到底要不要执行,例如:

package main

import "fmt"

func main() {
	i:=1
	if i==1{
		defer fmt.Println("defer")
	}
}

defer外面有一个if判断语句,当判断语句为true时,就会把对应的defer比特位设为1 。然后在函数末尾每个defer都会判断对应的比特位记录是否为1,如果为1就执行,否则就不执行。

defer_bit

使用时机

在当前版本中,defer一共有三种执行方式,那go到底是如何判断当前的defer是该用哪种方式呢?

代码生成阶段的 cmd/compile/internal/gc.state.stmt 会负责处理程序中的 defer,该函数会根据条件的不同,使用三种不同的机制处理该关键字:

func (s *state) stmt(n *Node) {
	...
	switch n.Op {
	case ODEFER:
		if s.hasOpenDefers {
			s.openDeferRecord(n.Left) // 开放编码
		} else {
			d := callDefer // 堆分配
			if n.Esc == EscNever {
				d = callDeferStack // 栈分配
			}
			s.callResult(n.Left, d)
		}func (s *state) stmt(n *Node) {
	...
	}
}

panic问题

虽然最新版本中的defer速度非常快,但是当程序发送panic时,在这之后的正常逻辑就都不会执行了,而是直接去执行defer链表。那些使用**开放地址(open coded)在函数内展开,因而没有被注册到链表的defer函数要通过栈扫描的方式来发现。所以1.14版本中就添加了几个字段用来辅助panic的栈扫描。

defer_114

回顾