Defer

defer是golang中一个很独特的关键字,用于创建延迟执行函数;经常被用来做处理函数退出之前的资源释放或者执行公共的代码逻辑;

在使用defer我们也经常遇到以下的一些问题,先看两个典型的案例:

如果可以提前了解golang闭包相关概念可以更好的理解defer的特性

1.defer/return问题

可以理解为先return,在执行执行defer,在没有提前声明返回参数时,会创建一个匿名参数;

2.参数预计算

这里更多的是受“闭包”特性的影响,函数+作用于内的变量;对于直接的函数调用(DeferArg1)来说参数通过复制直接传递到了所调用的函数,而对于匿名函数(DeferArg2)来说,可以当做一个闭包来看待,i所处的上下文属于外层函数(DeferArg2),和匿名函数在执行时,使用的i是同一个;

 

defer的实现(1.14)

简单描述一下1.13的实现,golang会在堆、栈上维护一个defer链表,在返回时按照栈的顺序递归执行每个defer;而保存没存每个defer对象需要包含以下信息:

function pointer, arguments, closure information等

这样导致defer在1.13上的表现并不满意,平均的处理时间为35ns;

 

在1.14中加入了open-coded开放编码模式,直接将将延迟defer函数的代码添加到函数尾部,来减少直接递归调用的开销;

1.14的优化,核心在ssa构建过程(看不懂的),我们主要关注defer对象的处理流程上的优化;

以下是1.13的实现:

在ssa构建阶段,编译器根据函数是否有defer语句,决定是否在exit阶段插入deferreturn代码;

而deferreturn的代码也主要分为三个阶段:参数处理,释放defer对象,执行defer函数并递归到下一个defer对象;

 

以下是1.14的实现:

在1.14中大部分的流程并没有改变,主要是在open-coded的基础上对deferreturn函数做了一定的调整;

在open-coded模式下,仅有一个defer对象fdvarpframepc uintptr等字段保存了所有的defer函数信息(函数指针、参数、上下文等),在runOpenDeferFrame仅通过一个遍历就执行了所有的defer函数;

 

deferBits(延迟比特)

根据open-code的描述,编译器需要在编译阶段将相关的defer函数代码插入到函数尾部,而特定的defer函数可能处于if,case分支中,需要在运行时确定是否需要执行defer函数;

deferBits就是为了这个问题,在执行过程中动态标记对应位的0/1值,来影响最终defer函数的执行情况;

由deferBits的初始化可以看出,其长度为8,也就是说open-coded模式,需要defer函数的总数不能超过8;

 

open-coded模式条件

这部分规则在ssa的相关代码中,感兴趣可以去查看,这里直接参考大佬们的结论;

  1. defer总数不超过8个
  2. return * defer < 15
  3. 非循环内使用defer
  4. gcflags包含 “N” ,则取消优化

 

总结

相对于1.13,1.14中新增了open-coded模式,在ssa阶段进行了优化,将递归的调用通过单次循环来完成;虽然使用open-coded模式有一定的条件,但是也可以看出大部分情况下是能满足这个条件的;

 

参考

https://github.com/golang/proposal/blob/master/design/34481-opencoded-defers.md

http://xiaorui.cc/archives/6579