GoLang之defer底层系列三(v 1.14)

GoLang之defer底层(Go1.14)

注:本文以Go SDK v1.14进行讲解

1.open coded defer放大招

减少_defer结构体的堆分配,也是1.14版本中defer性能优化要持续践行的策略。但是具体做法与1.13版本不同。

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){
    //......
}

上面这个例子中,函数A注册两个defer函数A1和A2,不过函数A2要到执行阶段根据条件判断是否要执行。先看defer函数A1这部分编译后的伪指令,Go1.14中会把A1需要的参数定义为局部变量,并在函数返回前直接调用A1。

func A(i int){
    var a, b int = i, 2*i
    //......
        
    A1(a, b)
    return
    //......
}

通过这样的方式不仅不用构建_defer结构体,也用不到defer链表,但是到defer函数A2这里就行不通了。因为A2不一定要被执行,这要在执行阶段根据参数i的值来决定。

Go1.14通过增加一个标识变量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
        A1(a, b)
    }
    return
    //......
}

所以像A2这样有条件执行的defer函数就可以像下面这样处理了。根据条件判断是否要把对应标识位置为1,函数返回前同样要根据标识符来判断是否要调用。

func A(i int){
    var df byte
    //A1的参数
    var a, b int = i, 2*i
    df |= 1

    //A2的参数
    var m,n string = "Hello", "eggo"
    if i > 1 {
        df |= 2
    }
    //code to do something
        
    //判断A2是否要调用
    if df&2 > 0 {
        df = df&^2
        A2(m, n)
    }
    //判断A1是否要调用
    if df&1 > 0 {
        df = df&^1
        A1(a, b)
    }
    return
    //省略部分与recover相关的逻辑
}

Go1.14把defer函数在当前函数内展开并直接调用,这种方式被称为open coded defer。这种方式不仅不用创建_defer结构体,也脱离了defer链表的束缚。不过这种方式依然不适用于循环中的defer,所以1.12版本defer的处理方式是一直保留的。

2.性能测试

接下来,我们使用如下代码进行性能测试:

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
}

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

go1.12,deferproc:


goos: windows
goarch: amd64
pkg: fengyoulin.com/research/defer_bench
BenchmarkDefer-8        30000000        41.1 ns/op
PASS

go1.13,deferprocStack:

 goos: windows
goarch: amd64
pkg: fengyoulin.com/research/defer_bench
BenchmarkDefer-8        38968154        30.2 ns/op
PASS

go1.14,open coded defer:

goos: windows
goarch: amd64
pkg: fengyoulin.com/research/defer_bench
BenchmarkDefer-8        243550725       4.62 ns/op
PASS

从deferproc到deferprocStack,约有25%的性能提升,而open coded defer几乎提升了一个数量级。

但是,必须要强调的是,我们一直在梳理的都是程序正常执行时defer的处理逻辑。一旦发生panic或者调用了runtime.Goexit函数,在这之后的正常逻辑就都不会执行了,而是直接去执行defer链表。那些使用open coded defer在函数内展开,因而没有被注册到链表的defer函数要通过栈扫描的方式来发现。

Go1.14中runtime._defer结构体又增加了几个字段:

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

借助这些信息,panic处理流程可以通过栈扫描的方式找到这些没有被注册到defer链表的defer函数,并按照正确的顺序执行。
所以,实际上Go1.14版本中defer的确变快了,但panic变得更慢了…

3.视频讲解

1.14版本,有什么不一样的优化策略呢?这一次一部分一部分的看。这里是函数A编译后的伪指令,我们略去一部分recover相关的内容。函数A有两个defer,我们先看deferA1,这里把函数A1需要的参数定义为局部变量,然后在函数返回前直接调用defer函数A1,用这样的方式,省去了构造defer链表项,并注册到链表的过程,也同样实现了defer函数延迟执行的效果。

image-20220309144020789

不过A2就不能这样简单处理了,它要到执行阶段才能确定是否需要被调用。Go语言用一个表示变量df来解决这个问题 。df里每一位对应标识一个defer函数时是否要被执行。例如这里第一个对应defer函数A1,A1需要执行所以通过或运算把df第一位置为1,defer函数调用这里,也要修改一个,先判断defer标识为是否是1,执行前,还要把df对应标识位置为0.避免重复执行。然后直接调用A1就好

image-20220309144115714

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

image-20220309144145248

Go1.14的defer就是通过在编译阶段插入代码,把defer函数执行逻辑展开在所属函数内。从而免于创建_defer结构体,而且不需要注册到defer链表。Go语言称这种方式为open coded defer

image-20220309144223412

但是同1.13一样,它依然不适用与循环中的defer,所以在这两个版本中,1.12版本的处理方式是一直保留的。通过性能测试三个版本的表现如上

image-20220309144245037

可以看到1.13版本性能提升了25%左右,与官方提供的数据出入不大,而1.14版本的性能几乎提升了一个数量级,但是这并非没有代价,我们一直在梳理的,都是程序正常执行的流程,如果发生panic或者调用runtime.Goexit()函数, 后面这些代码根本执行不到,就要去执行defer链表了;

而这些open coded方式实现的defer,并没有注册到链表,需要额外通过栈扫描的方式来发现,所以1.14版本中的_defer结构体,在1.13版本的基础上,又增加了几个字段,借助这些信息,可以找到未注册到链表的defer函数。并按照正确的顺序执行 ;
所以实际上1.14版本中,defer的确变快了,但panic变得更慢了,但是Go语言做出这样的优化,一定是综合考量了整体性能,毕竟panic发生的几率要比defer低

image-20220309144340752

image-20220309144620899

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoGo在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值