暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

go语言系列6 - 深度解读defer

栋总侃技术 2021-04-19
1295

我们还是从代码示例着手来了解Go语言的defer是如何实现的,首先我们看下案例一:

    func main() {
    i := 0
      defer fmt.Println(f1(i))
    i = i + 2
    }

    func f1(i int) int {
    return i + 1
    }

    案例一的输出是 1,我们再看案例二:

      func main() {
        i := 0
      defer func() {
      fmt.Println(f1(i))
      }()
      i = i + 2
      }

      func f1(i int) int {
      return i + 1
      }

      案例二的输出是3。

      怎么解释上述输出的不同呢?我们先看下defer的定义:Go 语言的 defer
       会在当前函数返回前执行传入的函数。在这里我们一般都关注了“会在当前函数返回器执行”而忽略了“函数”,所以我特别地把函数标红加粗。而在调用用 defer 关键字的时候,会立即拷贝调用该函数的参数

      在案例一中的“函数”是 fmt.Println(),其内部引用的参数是 f1方法的返回值。此时立刻把调用f1()函数的返回值 1 拷贝下来(此时main函数还未运行到第4行,i的值仍然为0,f1()  的返回值为1),在main方法返回前相当于调用fmt.Println(1),

      而在案例二中“函数”是匿名函数,其内部引用的参数是该匿名函数的指针。当我们把案例二中的匿名函数命名,其代码类似如下:

        import "fmt"

        func main() {
          i := 0
        f2 := func() {
        fmt.Println(f1(i))
        }
        defer f2()
        i = i + 2
        }

        func f1(i int) int {
        return i + 1
        }

         此时 f2 函数是没有参数的,不需要对参数进行拷贝,而函数继续运行到最后 main 函数要退出前会调用f2方法,而此时i已经变成 2 了,再执行 f1() 返回的结果是3,所以案例二输出的结果是3。

        defer关键字的结构定于在Go源码的runtime/runtime2.go文件中:

          // A _defer holds an entry on the list of deferred calls.
          // If you add a field here, add code to clear it in freedefer and deferProcStack
          // This struct must match the code in cmd/compile/internal/gc/reflect.go:deferstruct
          // and cmd/compile/internal/gc/ssa.go:(*state).call.
          // Some defers will be allocated on the stack and some on the heap.
          // All defers are logically part of the stack, so write barriers to
          // initialize them are not required. All defers must be manually scanned,
          // and for heap defers, marked.
          type _defer struct {
          siz int32 // includes both arguments and results
          started bool
          heap bool
          // openDefer indicates that this _defer is for a frame with open-coded
          // defers. We have only one defer record for the entire frame (which may
          // currently have 0, 1, or more defers active).
          openDefer bool
          sp uintptr // sp at time of defer
          pc uintptr // pc at time of defer
          fn *funcval // can be nil for open-coded defers
          _panic *_panic // panic that is running defer
          link *_defer

          // If openDefer is true, the fields below record values about the stack
          // frame and associated function that has the open-coded defer(s). sp
          // above will be the sp for the frame, and pc will be address of the
          // deferreturn call in the function.
          fd unsafe.Pointer // funcdata for the function associated with the frame
          varp uintptr // value of varp for the stack frame
          // framepc is the current pc associated with the stack frame. Together,
          // with sp above (which is the sp associated with the stack frame),
          // framepc/sp can be used as pc/sp pair to continue a stack trace via
          // gentraceback().
          framepc uintptr
          }

          我们看到该结构中有个 link 字段指向调用链路的上一个 _defer。我们可以把Go的defer看成一个链表。

          • fn:指向调用函数的指针,案例一中的fmt.println(),案例三中的f2()。

          • siz:defer函数的入参和返回值空间大小,其作用表现在 _defer 紧跟着的siz空间存储fn函数的入参,和返回值。

          • heap:用来区分 _defer 在栈上分配还是在堆上分配。

          其他参数有一部分在GC时使用的。

          在Go1.3的Release Note中有这么一句话,其大概意思就是在1.3对defer提升了30%的效率。

              This release improves performance of most uses of defer
           by 30%.

          我们来看下Go1.3相对于之前的版本对defer进行了那些优化的呢?我们来看下在Go1.3之后的版本中的一段源码:

          可以看到在有些条件下栈上申请_defer空间,而有些时候在堆上申请空间,同时还有一个open-coded类型,这个我们在后文中介绍。在Go1.3以前_defer都是在堆上分配的,而在Go1.3以后可以在栈上分配_defer空间(绝大多数场景),所以其效率高很多。

          那在栈上分配_defer空间的条件是什么呢?可以看代码是Esc==EscNever,我们再搜下代码EscNever怎样会被负值,可以搜到这段代码:

          由于没有系统的去读这块代码,不清楚含义,但是我通过loopDepth来用塑料英语再翻译一下,表示循环的深度,难道就是循环的深度为1才在栈上分配_defer结构的内存?

          然后我去网上搜了下资料,loopDepth确实是用来检测循环层次的,那也就是说嵌套的循环作用域里的_defer将不会被分配在栈上。  这种情况确实在使用defer的时候比较少见,所以绝大多数场景下的_defer将会分配到栈上。

          那么,我们在使用defer的时候需要注意了:在使用defer时,其外层尽量不要使用嵌套的for循环,这样将会大大的降低defer的运行效率

          而我们在上文中除了在堆栈中分配_defer,还提到了open_coded,那这又是什么呢?这就是在Go1.4及其以后引入了开放编码。开放编码就是Go会在编译期间就把defer需要执行的代码逻辑直接插入到函数的尾部,则在运行时不需要再有defer什么事了。

          open_coded在Go1.4中的引用同时也是对defer的性能进行了进一步的提升。而在苛刻的情况下才会启用open_coded,在下一节我们将详细讲解open_coded的开启和实现机制。

          往期回顾:

          go语言系列1 - Goroutine的实现与调度

          go语言系列2 - 剖析Channel的实现原理

          go语言系列3 - Channel应用流水线模型

          go语言系列4 - Goroutine、channel的使用还有话说

          go语言系列5 - 你不得不知道slice的那些事

          文章转载自栋总侃技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论