Golang知识点七、defer

defer

  从本篇文章开始记录defer相关的知识点,defer相关的内容包括三部分,分别是defer注册、defer执行和defer优化策略。

1. defer注册

  defer会在函数返回之前倒序执行,下面是一段go代码,及其在编译后的伪指令描述。

	func A() {
		defer B()
		// code to do something
	}
	func A() {
			r = deferproc(8, B)
			if r > 0 {
				goto ret
			}
			
			// code to do something
	
			runtime.deferreturn()
		ret:
			runtime.deferreturn()
	}

  defer指令对应到两部分内容,deferproc负责把要执行的函数信息保存起来,称为defer注册。defer注册完成后,继续执行后面的逻辑,直到返回前通过deferreturn执行注册的defer函数。正是由于先注册,后调用的机制,才实现了defer延迟 执行的效果。

  defer信息会注册到一个链表,当前执行的goroutine持有这个链表头指针,将一个个_defer结构体进行链接,新注册的defer会添加到链表头,执行时也是从头开始,这也是defer表现为倒序执行的原因。

	func deferproc(siz int32, fn *funcval)

  deferproc函数原型只有两个参数,siz指defer函数的参数和返回值共占用的内存空间大小,fn 是一个function value。没有捕获列表的function value编译器会做出优化,即在只读数据段分配一个共用的funcval结构体。

	type _defer struct {
		siz int32		// 参数和返回值大小(字节),注册时保存参数,执行时拷贝到调用者参数和返回值空间
		started bool    // defer是否已经执行
		sp uintptr		// 注册这个defer的函数栈指针,通过它函数可以判断自己注册的defer是否已经执行完
		pc uintptr		// deferproc的返回地址
		fn *funcval		// 注册的函数
		_panic *_panic	
		link *_defer	// 前一个注册的_defer结构体
	}

  deferproc函数调用时,编译器会在它自己的两个参数后面开辟一段空间,用于存放defer函数的参数和返回值。deferproc执行时,需要对分配一段空间,用于存放_defer结构体以及参数和返回值。实际上,go语言会预分配不同规格的deferpool,执行时从空闲defer中取出一个来用,如果没有合适大小的_defer,再进行堆分配。

2. defer执行

  执行defer函数时,从当前goroutine拿到链表头上的这个_defer结构体,通过fn找到funcval,拿到函数入口地址,调用defer函数时,会把_defer结构体后面的参数和返回值整个拷贝到A1的调用者栈上。要注意,defer函数的参数,在注册时拷贝到堆上,执行时又拷贝到栈上。

	func A1(a int) {
		fmt.Println(a) 		// 1
	}
	func A() {
		a, b := 1, 2
		defer A1(a)
		
		a = a + b
		fmt.Println(a,b) 	// 3, 2
	}

  到这里已经很明了,deferproc的目的是注册一个function value结构体。那么,有捕获列表的情况下,会是一个怎样的过程呢?闭包函数会在执行阶段,根据代码段的闭包指令,创建闭包对象。如果捕获变量除了初始化赋值还被修改过,会进行局部变量的堆分配,栈上只保存变量在堆上的地址。deferproc执行时,_defer结构体中的fn,保存闭包函数funcval结构体的起始地址,另外还要拷贝参数到_defer结构体后面,把_defer结构体添加到defer链表头。函数执行完毕,到deferreturn时,defer函数执行,首先把参数b拷贝到栈上的参数空间,再执行后续操作。关键要理解defer传参和闭包捕获变量的实现机制。

	func A() {
		a, b := 1, 2
		defer func(b int) {
			a = a + b
			fmt.Println(a, b)	// 5, 2
		}(b)
	}
	a = a + b
	fmt.Println(a, b)			// 3, 2

  go 1.12版本的defer设计有一个明显的问题,慢!主要原因如下,首先是_defer结构体堆分配,即使有预分配的deferpool也需要去堆上分配或释放,且defer函数传参还要在堆栈间进行拷贝。其次,使用_defer链表来注册当前goroutine的defer信息,而链表结构本身操作比较慢。于是,go1.13版本和go1.14版本中对defer进行了优化。

3. defer优化策略

  Go1.12局限性在于,通过deferproc函数注册defer函数信息,defer结构体分配在堆上。

3.1. go 1.13版本优化策略

如下代码,go1.12和go 1.13版本以及编译后的指令。

	func A() {
		defer B(10)
		// code todo sth
	}
	func B(i int) {
		...
	}
	// go 1.12版本,编译后伪指令
	func A() {
			r := runtime.deferproc(8, B)
			if r > 0 {
				goto ret
			}
			// code to do something
	
			runtime.deferreturn()
		ret:
			runtime.deferreturn()
	}
	// go1.13版本,编译后伪指令
	func A() {
			// 通过在编译阶段增加局部变量,把defer信息保存到当前函数栈帧的局部变量区域。
			var d struct {
				runtime._defer
				i int
			}
			d.siz = 0
			d.fn = B
			d.i = 10
			
			// 通过deferprocStack把栈上这个_defer结构体,注册到defer链表中。
			r := runtime.deferprocStack(&d._defer)
			if r > 0{
				go to ret
			}
			// code to do sth
			runtime.deferreturn()
			return
		ret:
			runtime.deferreturn()
	}

  go 1.13中defer优化点主要在减少defer信息的堆分配,之所以说是减少,是因为在显式循环或隐式循环中,依然使用1.12中的方案,在堆上分配,因此_defer结构体中增加了一个字段,heap bool用于标识是否为堆分配。

	// 显式循环
	for i := 0; i < n; i++ {
		defer B(i)
	}
	
	// 隐式循环
	again:
		defer B()
		if i < n {
			n++
			goto again
		}
	type _defer struct {
		siz int32		// 参数和返回值大小(字节),注册时保存参数,执行时拷贝到调用者参数和返回值空间
		started bool    // defer是否已经执行
		heap bool		// 是否堆分配
		sp uintptr		// 注册这个defer的函数栈指针,通过它函数可以判断自己注册的defer是否已经执行完
		pc uintptr		// deferproc的返回地址
		fn *funcval		// 注册的函数
		_panic *_panic	
		link *_defer	// 前一个注册的_defer结构体
	}

  在go1.13中,通过在编译阶段增加局部变量,将_defer结构体信息保存到当前函数栈帧的局部变量区域,再通过deferprocStack把栈上这个_defer结构体注册到defer链表中。defer执行时,依然通过deferreturn实现的,也同样要在defer函数执行时拷贝参数,这次不是在堆栈间,而是从栈上的局部变量空间拷贝到参数空间。

3.2. Go1.14版本优化策略

  如下go语言代码,编译后的产生的指令。

	func A(i int) {
		defer A1(i, 2*ij)
		// code to do sth
		if i > 1 {
			defer A2("hello")
		}
		// code to do sth
		return
	}
	func A1(a, b int) {
		...
	}
	func A2(m, n string) {
		...
	}
	func A(i int) {
		// df每一位,对应标识一个defer函数是否要被执行
		var df byte
		var a, b int = i, 2*i
		
		// code to do sth
		
		var m, n string = "hello","eggo"
		// 通过或运算把df第一位置为1
		df |= 1

		// 根据具体条件,判断df第2个标识位是否要被置为1
		if i > 1 {
			df |= 2
		}
		
		// code to do sth
		
		// 依据第二个标识位,判断是否要
		if df & 2 > 0 { 
			// 函数返回前也要依据第二个标识位,决定是否要调用函数A2
			df = df &^ 2
			A2(m, n)
		}
		
		// 判断defer标识位是否为1
		if df & 1 > 0 {
			// 执行前,把df对应标识位置为0
			df = df&^ 1
			A1(a, b)
		}
		return
	}

  Go1.14的defer就是在编译阶段插入代码,把defer函数的执行逻辑展开在所属函数内,从而免于创建_defer结构体,而且不需要注册到defer链表,这种方式称为open coded defer。go1.14版本和go1.13版本一样,它依然不适合循环中的defer。因此,在go1.13和go

  go1.14版本相比前两个版本提升了一个数量级,但是在代码发送panic或调用runtime.Goexit时,由于后面的defer函数代码执行不到,需要通过栈扫描的方式来发现,因此go1.14版本中,defer确实变快了,但是panic却变慢了,这也是go团队综合考虑的结果,毕竟panic发生的几率要比defer发生的几率小很多。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值