go语言defer系列(四)—— 栈分配及内联优化

863 阅读2分钟

「这是我参与2022首次更文挑战的第21天,活动详情查看:2022首次更文挑战」。

前言

本文主要介绍defer如何在栈中存储。

栈分配

栈分配的过程包括deferprocStack,deferreturn

deferprocStack源码如下:

func deferprocStack(d *_defer) {
   gp := getg()
   if gp.m.curg != gp {
      throw("defer on system stack")
   }
   if goexperiment.RegabiDefer && d.siz != 0 {
      throw("defer with non-empty frame")
   }
   d.started = false
   d.heap = false
   d.openDefer = false
   d.sp = getcallersp()
   d.pc = getcallerpc()
   d.framepc = 0
   d.varp = 0
   *(*uintptr)(unsafe.Pointer(&d._panic)) = 0
   *(*uintptr)(unsafe.Pointer(&d.fd)) = 0
   *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
   *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))

   return0()
}

底下那部分冗长且有指针的操作其实只是对defer结构体的赋值,并把defer挂在当前协程上,这么写的好处是没有写屏障,至于什么是写屏障留到后面垃圾回收算法细说。

相较于堆分配需要在这步在堆内存中创建一个defer结构体并为其复制,栈内存更加迅捷一点。

内联优化

相较于栈分配,是否可以进一步优化呢?

最便捷的方式就是像函数一样直接调用

举个例子

func f(){
    deferproc a()
    deferproc b()
    c()
    
    deferreturn()
}

可以近似变成

func f(){
    c()
    b()
    a()
}

我们考虑deferproc a()来记录所需参数,但是依旧有一个问题就是deferreturn的时候不确定这个defer是否需要执行。因为可能会出现defer b()被包裹在if里。在编译时无法确定是否执行这个defer的情况下将defer放入链表中显然是不健康的

在go语言中,Go语言编辑器采取了一种巧妙的方式,以位图的方式来进行判断。

func f(){
    defer a()
    if isTrue {
        defer b()
    }
    c()
}

变为

func f(){
    deferBits |= 1<<0
    tmpF1 = a
    if isTrue{
        deferBits |= 1<<1
        tmpF2 = b
    }
    c()
    
    //deferreturn 区
    if deferBIts & 1<<1 != 0 {
        tmpF2()
    }
     if deferBIts & 1<<0 != 0 {
        tmpF1()
    }
}

通过位图的方式将if判断条件与是否执行defer相解耦,以最小的代价满足了大部分情况下的需求。