当前位置: 首页 > article >正文

【新人系列】Golang 入门(九):defer 详解 - 下

✍ 个人博客:https://blog.csdn.net/Newin2020?type=blog
📝 专栏地址:https://blog.csdn.net/newin2020/category_12898955.html
📣 专栏定位:为 0 基础刚入门 Golang 的小伙伴提供详细的讲解,也欢迎大佬们一起交流~
📚 专栏简介:在这个专栏,我将带着大家从 0 开始入门 Golang 的学习。在这个 Golang 的新人系列专栏下,将会总结 Golang 入门基础的一些知识点,并由浅入深的学习这些知识点,方便大家快速入门学习~
❤️ 如果有收获的话,欢迎点赞 👍 收藏 📁 关注,您的支持就是我创作的最大动力 💪

1. 函数嵌套

我们再来看一个形如 defer(A(B©) 这样的例子,看看 defer 执行时参数 a 到底等于多少。

func B(a int) int {
    a++
    return a
}
func A(a int) {
    a++
    fmt.Println(a)
}
func main() {
    a := 1
    defer A(B(a))
    a++
    fmt.Println(a)
}

这个例子中,main 函数注册的 defer 函数是 A,所以 defer 链表中 _defer.fn 存储的是 A 的 funcval 指针。但是在 deferproc 执行时,需要保存 A 的参数到 _defer 结构体的后面,因此需要在这时候就拿到 defer 函数注册时 A 传入的参数,即需要此刻计算出 B(1) 的返回值作为参数传入。

在这里插入图片描述

所以 A 最终会得到 B(1) = 2 的参数进行传入,又因为该参数不存在捕获的条件即不会形成闭包,也就等于传入的这个参数固定下来了,后面再去修改参数 a 也不会影响到 defer 函数最终的结果。因此,最终 defer A(B(a)) 将会输出 3。

2. defer 嵌套

最后再来看一个 defer 嵌套的例子,这次抛开所有细节只看 defer 链表随着 A 的执行会怎样变化。

func A() {
    //......
    defer A1()
    //......
    defer A2()
    //......
}
func A2() {
    //......
    defer B1()
    defer B2()
    //......
}
func A1() {
    //......
}
//所有defer函数都正常执行......

首先函数 A 注册了两个 defer,这里途中我们用 A1 和 A2 函数名进行标记。

在这里插入图片描述

在函数 A 返回前执行 deferreturn 时,会判断 defer 链表头上的 defer 是不是 A 注册的。而这个方法就是判断 defer 结构体记录的 sp 是否等于 A 的栈指针,即判断 A2.sp == SP of A 是否成立。

如果是 A 注册的,就会保存函数的相关信息,就会把它从 defer 链表中移除并执行函数 A2,此时又会注册两个 defer,并记为 B1 和 B2。

在这里插入图片描述

在 A2 返回前同样会去执行 defer 链表,同样判断是否是自己注册的 defer 函数,即判断 B2.sp == SP of A2 是否成立。然后 B2 执行,同样的流程 B1 执行。此时 A2 仍不知道自己注册的 defer 已经执行完了,直到下一个 _defer.sp 不等于自己的栈指针,然后 A2 就可以结束了。

在这里插入图片描述

再次回到 A 的 defer 执行流程,然后执行 A1,当 A1 执行结束后 defer 链表为空,函数 A 结束。这个例子的关键是 defer 链表注册时添加链表项,执行时移除链表项的用法。

至此 go 1.12 版本的 defer 基本设计思路就梳理完了,这个版本的 defer 一个明显的问题就是慢。

  • 其中一个原因是 _defer 结构体堆分配,即使有预分配的 deferpool,也需要去堆上获取与释放,而且参数还要在堆栈之间来回拷贝。
  • 第二个原因是使用链表注册 defer 信息,而链表本身操作就比较慢。
    所以 go 在 1.13 和 1.14 中分别作了不同的优化。

3. defer 1.13

go 1.13 中 defer 性能的优化点,主要集中在减少 defer 结构体堆分配。我们来看一个例子,看看具体是如何做到的。

func A() {
    defer B(10)
    // code to do something
}
func B(i int) {
    //......
}

在 go 1.12 版本中,编译后的伪指令如下。

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

而在 go 1.13 版本中,编译后的伪指令发生了改变,如下所示。

func A() {
    var d struct {
        runtime._defer
        i int
    }
    d.siz = 0
    d.fn = B
    d.i = 10
    r := runtime.deferprocStack(&d._defer)
    if r > 0 {
        goto ret
    }
    // code to do something
    runtime.deferreturn()
    return
 ret:
    runtime.deferreturn()
}

同样,我们先忽略掉错误处理的部分,看看 go 1.13 做出了怎样的优化。

func A() {
    var d struct {
        runtime._defer
        i int
    }
    d.siz = 0
    d.fn = B
    d.i = 10
    r := runtime.deferprocStack(&d._defer)

    // code to do something
    runtime.deferreturn()
    return

}

在 1.12 中是通过 deferproc 注册 defer 函数信息,并将 _defer 结构体分配到堆上。

而在 1.13 中,通过在编译阶段增加上面结构体 d 这样的局部变量,把 defer 信息保存到当前函数栈帧的局部变量区域,然后再通过 deferprocstack,把栈上这个 _defer 结构体注册到 defer 链表中。

在这里插入图片描述

defer 1.13 的优化点主要就在于减少 defer 信息的堆分配。之所以说减少,是因为像下面这样的显示循环或隐式循环中的 defer 依然需要使用 1.12 版本的处理方式,只能在堆上分配,即使只执行一次 for 循环也是一样。

//显示循环
for i:=0; i< n; i++ {
    defer B(i)
}

//隐式循环
again:
    defer B()
    if i<n {
        n++
        goto again
    }

为此 _defer 结构体中增加了一个字段 heap,用于标识是否为堆分配。

type _defer struct {
    siz       int32
    started   bool
    heap      bool       //标识是否为堆分配
    sp        uintptr
    pc        uintptr
    fn        *funcval
    _panic    *_panic
    link      *_defer
}

不过 defer 函数的执行在 1.13 中没有变化,依然通过 deferreturn 实现,依然需要把 _defer 结构体后面的参数与返回值空间,拷贝到 defer 函数的调用者栈上。只不过不是从堆上拷贝到栈上,而是从栈上的局部变量空间拷贝到参数空间。

在这里插入图片描述

1.13 的 defer 官方提供的性能提升是 30%,我们来看看 1.14 又有什么样的优化策略。

4. defer 1.14

为了能更清楚的理解 1.14 的优化策略,我们这次案例一部分一部分来看。

func A(i int) {
    defer A1(i, 2*i)
    if(i > 1) {
        defer A2("Hello", "eggo")
    }
    // code to do something
    return
}
func A1(a,b int) {
    //......
}
func A2(m,n string) {
    //......
}

先来看看 A1 的伪指令,去掉错误处理的相关操作,这里会把 defer 函数 A1 需要的参数定义为局部变量,然后在函数返回前正常调用 defer 函数 A1。用这样的方式,可以省去构造 defer 链表项并注册到链表的过程,也同样实现了 defer 函数延迟执行的效果。

func A(i int){
    var a, b int = i, 2*i
    
    // code to do something
        
    A1(a, b)
    return
}

不过 A2 就不能这样处理了,它要到执行阶段才能确定是否需要被调用。go 语言会用一个标识变量 df 来解决这个问题,df 里每一位对应标识一个 defer 函数是否需要被执行。

在这里插入图片描述

例如,这里的函数 A1 要被执行,所以就通过 df |= 1 把 df 第一位置为 1;在函数返回前这里也要修改一下,需要通过 df & 1 判断是否要调用函数 A1。

func A(i int){
    var df byte
    var a, b int = i, 2*i
    df |= 1
    
    // code to do something
        
    if df & 1 > 0 {
        df = df &^ 1    // 执行前把df对应的标识位置为0,避免重复执行
        A1(a, b)
    }
    return
}

同样的方式到 A2 这里,在程序执行阶段就会根据具体条件,判断 df 第二个标识位是否要被置为 1。对应的函数返回前,也要依据第二个标识位来决定是否要调用函数 A2。

func A(i int){
    var df byte
    var a, b int = i, 2*i
    
    // code to do something
    var m, n string = "Hello", "eggo"
    
    df |= 1
    if i > 1 {
        df |= 2
    }
        
    if df & 2 > 0 {
        df = df &^ 2    // 执行前把df对应的标识位置为0,避免重复执行
        A2(m, n)
    }
        
    if df & 1 > 0 {
        df = df &^ 1    // 执行前把df对应的标识位置为0,避免重复执行
        A1(a, b)
    }
    return
}

go 1.14 的 defer 就是通过在编译阶段插入代码,把 defer 函数的执行逻辑展开在所属函数内,从而免于创建 _defer 结构体,而且也不需要注册到 defer 链表。

go 语言称这种方式为 open coded defer,但是跟 1.13 一样,它依然不适用于循环中的 defer。所以在这两个版本中,1.12 版本的处理方式是一直保留的。

5. 性能测试

我们可以通过性能测试来看看三个版本的表现如何,假设用下面这段代码进行性能测试。

func BenchmarkDefer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Defer(i)
    }
}
func Defer(i int) (r int) {
    defer func() {
        r -= 1
        r |= r>>1
        r |= r>>2
        r |= r>>4
        r |= r>>8
        r |= r>>16
        r |= r>>32
        r += 1
    }()
    r = i * i
    return
}

这三个版本的测试结果如下所示:

  • go 1.12,deferproc:
goos: windows
goarch: amd64
pkg: Desktop/project/test
BenchmarkDefer-8        30000000        41.1 ns/op
PASS
  • go 1.13,deferprocStack:
goos: windows
goarch: amd64
pkg: Desktop/project/test
BenchmarkDefer-8        38968154        30.2 ns/op
PASS
  • go 1.14,open coded defer:
goos: windows
goarch: amd64
pkg: Desktop/project/test
BenchmarkDefer-8        243550725       4.62 ns/op
PASS

可以发现 1.13 版本的性能提升了 25% 左右,而 1.14 版本的性能直接几乎提升了一个数量级。

但是这也需要付出一些代价,我们一直在梳理的都是程序正常执行的流程,如果在代码中发生了 panic 或者调用 runtime.Goexit() 函数,其后的代码逻辑就压根执行不到就去执行 defer 链表。而这些 open coded 方式实现的 defer 并没有注册到链表,需要额外通过栈扫描的方式来发现。

所以 1.14 _defer 结构体在 1.13 版本的基础上,又增加了几个字段,借助这些信息就可以找到未注册到链表的 defer 函数,并按照正确的顺序执行。

type _defer struct {
    siz       int32
    started   bool
    heap      bool       
    openDefer bool
    sp        uintptr
    pc        uintptr
    fn        *funcval
    _panic    *_panic
    link      *_defer
    fd        unsafe.Pointer
    varp      uintptr
    framepc   uintptr
}

因此在 1.14 版本中,defer 虽然变快了,但 panic 反而变得更慢了。不过 go 语言选择做出这样的优化,肯定是综合考量了整体的性能,毕竟 panic 发生的几率要比 defer 低。


http://www.kler.cn/a/612413.html

相关文章:

  • FPGA_YOLO学习(一)
  • 【HTML】KaTeX 常用公式字符
  • 问题分析4
  • 数据结构与算法:双向广搜
  • 第六届 蓝桥杯 嵌入式 省赛
  • ​​SenseGlove与Aeon Robotics携手推出HEART项目,助力机器人培训迈向新台阶
  • uniapp自定义目录tree(支持多选、单选、父子联动、全选、取消、目录树过滤、异步懒加载节点、v-model)vue版本
  • 免费使用!OpenAI 全量开放 GPT-4o 图像生成能力!
  • QT记事本
  • RISC-V AIA学习3---APLIC 第二部分(APLIC 中断域的内存映射控制区域)
  • 【软测】AI助力测试用例
  • 快速入手-基于Django-rest-framework的ModelSerializer模型序列化器(三)
  • 华为、浪潮、华三链路聚合概述
  • python使用cookie、session、selenium实现网站登录(爬取信息)
  • 用 Python 也能做微服务?
  • Vue+SpringBoot:整合JasperReport作PDF报表,并解决中文不显示问题
  • OPENCV数字识别(非手写数字/采用模板匹配)
  • jEasyUI 创建自定义视图
  • EMC知识学习二
  • 路由选型终极对决:直连/静态/动态三大类型+华为华三思科配置差异,一张表彻底讲透!