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 somethingruntime.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, 2defer A1(a)a = a + bfmt.Println(a,b)  // 3, 2}

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

 func A() {a, b := 1, 2defer func(b int) {a = a + bfmt.Println(a, b)  // 5, 2}(b)}a = a + bfmt.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 somethingruntime.deferreturn()ret:runtime.deferreturn()}
 // go1.13版本,编译后伪指令func A() {// 通过在编译阶段增加局部变量,把defer信息保存到当前函数栈帧的局部变量区域。var d struct {runtime._deferi int}d.siz = 0d.fn = Bd.i = 10// 通过deferprocStack把栈上这个_defer结构体,注册到defer链表中。r := runtime.deferprocStack(&d._defer)if r > 0{go to ret}// code to do sthruntime.deferreturn()returnret: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 sthif i > 1 {defer A2("hello")}// code to do sthreturn}func A1(a, b int) {...}func A2(m, n string) {...}
 func A(i int) {// df每一位,对应标识一个defer函数是否要被执行var df bytevar a, b int = i, 2*i// code to do sthvar m, n string = "hello","eggo"// 通过或运算把df第一位置为1df |= 1// 根据具体条件,判断df第2个标识位是否要被置为1if i > 1 {df |= 2}// code to do sth// 依据第二个标识位,判断是否要if df & 2 > 0 { // 函数返回前也要依据第二个标识位,决定是否要调用函数A2df = df &^ 2A2(m, n)}// 判断defer标识位是否为1if df & 1 > 0 {// 执行前,把df对应标识位置为0df = df&^ 1A1(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发生的几率小很多。

Golang知识点七、defer相关推荐

  1. php函数知识点,php入门学习知识点七 PHP函数的基本应用_php基础

    /* * 简单的函数 */ function fontBold($con){ return "$con"; } $str="简单的函数测试!"; echo &q ...

  2. Golang脱俗的defer

    Golang脱俗的defer 浅谈defer GO中的defer一直都是项目中常用的依赖.无论是解锁,还是关闭文件,或者关闭session,大多都离不开defer.稍微使用过golang的粉(huan ...

  3. C语言指针基础知识点(七)--通过指针引用字符串

    指针系列目录   C语言指针基础知识点(一)–指针及指针变量   C语言指针基础知识点(二)–指针变量的引用   C语言指针基础知识点(三)–指针变量作为函数参数   C语言指针基础知识点(四)–通过 ...

  4. 七年级计算机考试知识点,七年级语文期中考试复习知识点整理

    合理的总结,合理的归纳,对于考试成绩会有很大的帮助,下文为大家推荐了七年级语文期中考试复习知识点,祝大家期中考试顺利. .文章体裁 此文是一篇:诗歌.小说.散文(抒情散文.叙事散文).剧本.说明文.议 ...

  5. 七年级计算机考试知识点,七年级语文重点笔记 必考知识

    很多同学都对初中的知识十分好奇,那么七年级语文都有哪些知识点?初一又有哪些高频考点?大家一起来看看那吧. 初中语文知识点 一.句子含义的解答 这样的题目,句子中往往有一个词语或短语用了比喻.对比.借代 ...

  6. Golang知识点总结

    数据结构 Context Context的调用链: 和链表有点像,只是它的方向相反:Context 指向它的父节点,链表则指向下一个节点 重要概念:(源码位置:src/context/context. ...

  7. 计算机七年级知识点,七年级信息的技术演示课件——计算机与信息的技术基本知识点.ppt...

    七年级信息的技术演示课件--计算机与信息的技术基本知识点 信息技术课件 1.1 计算机概述 1.3.4 计算机硬件系统 1.2.1 数字化信息编码的概念 1.2.2 进位计数制 ①八进制:0.2.3. ...

  8. 七年级上册计算机重点知识点,七年级第一学期信息技术复习知识点1

    七年级第一学期信息技术复习知识点1 1) 七年级第一学期信息技术复习知识点(1) 第一课 信息与信息技术 1的客观描述. 2.信息可以借助文字.图像.声音.数字.影像等载体来呈现和传播. 3 4.信息 ...

  9. PMP知识点(七、资源管理)

    此系列文章分享给想学习PMP的项目经理和想要学习PMP的程序猿们!期望观看者快速掌握PMP知识点并实际运用(还有顺利考过PMP了). 上次写文章还是两年以前了,这期间工作.身体都有些变故,最主要的是我 ...

最新文章

  1. Spring Boot 解决跨域问题的 3 种方案!
  2. linux ulimit命令 控制shell执行程序的资源
  3. C/C++之常用关键字
  4. mybatis的逆向工程
  5. mysql主从技术_MySQL主从架构的实现
  6. 如何识别交换机的性能优劣?
  7. 解读浮动闭合最佳方案:clearfix
  8. d3 tip mysql_mysql
  9. php 伪静态规则,在线将Apache Rewrite Rules伪静态规则转换为Nginx Rewrite伪静态规则...
  10. 微博预期12月8日登陆港交所 最终发售价定为272.8港元
  11. 机器人学中的状态估计 中文版_《机器人学中的状态估计》-05偏差,匹配和外点...
  12. 晶振为什么不封装进芯片内部?
  13. 折腾BIOS,改开机logo图标
  14. blender做MMD心得(一)
  15. 奉劝那些想把编程学好的学弟学妹们!呕心沥血,袒露心声,掏心掏肺
  16. JS 实现列表移动(JQuery实现)
  17. Linux蓝牙鼠标自己断开,thinkpad蓝牙鼠标经常断线怎么办 thinkpad蓝牙鼠标频繁断开连接处理方法...
  18. 2. 表的操作:创建表、修改表、列约束和表约束、数据操作、删除表
  19. MATLAB水箱液位模糊控制仿真程序
  20. 一键adb关闭系统更新 坚果手机_华为手机通过ADB永久关闭系统更新

热门文章

  1. [ARC085]F - NRE RMQ优化DP
  2. idea启动报错Native memory allocation (malloc) failed to allocate
  3. Linux驱动开发(硬件基础知识)——存储器
  4. 【Leetcode -1721.交换链表中的节点 -2058.找出临界点之间的最小和最大距离】
  5. 怎么把淘宝长链接转换成可以在新浪微博,微信发布的短链接?
  6. WikiOI 3269 混合背包 (动规+多重背包优化)
  7. jQuery 将本地时间转换成 UTC 时间,计算时差,将UTC时间转换成 本地 时间
  8. Android进度条/等待加载——旋转小圆点效果
  9. 幽默夫妻笑话-弱不禁风的妻子
  10. JavaScript入门 放大镜案例 / 单页应用路由开发 Day23