golang 源码剖析(7): 延迟defer

darcyaf · · 556 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

简介

延迟调用(defer)的优势是:

  1. 即使函数执行出错,依然能保证回收资源等操作得以执行
  2. 可以在变量的定义处加入defer,代码结构上避免忘记做某些数据的回收

劣势:

  1. 性能上会会比直接调用慢一些
  2. 如果在defer中释放,相对来说只会在函数执行结束的时候才会调用,变量生命周期会变长.

定义

编写以下程序, dump出汇编.
defer主要调用了一下两个函数func deferprocStack(d *_defer)func deferreturn(arg0 uintptr)

package main

import (
    "fmt"
)

func main() {
    defer fmt.Println(0x11)
}
(base) ➜  readsrc go tool objdump -s "main\.main" ./test
TEXT main.main(SB) /home/darcyaf/Development/go/src/readsrc/main.go
  main.go:7             0x48cf30                64488b0c25f8ffffff      MOVQ FS:0xfffffff8, CX
  main.go:7             0x48cf39                488d4424d8              LEAQ -0x28(SP), AX
  main.go:7             0x48cf3e                483b4110                CMPQ 0x10(CX), AX
  main.go:7             0x48cf42                0f86b1000000            JBE 0x48cff9
  main.go:7             0x48cf48                4881eca8000000          SUBQ $0xa8, SP
  main.go:7             0x48cf4f                4889ac24a0000000        MOVQ BP, 0xa0(SP)
  main.go:7             0x48cf57                488dac24a0000000        LEAQ 0xa0(SP), BP
  main.go:8             0x48cf5f                0f57c0                  XORPS X0, X0
  main.go:8             0x48cf62                0f11842490000000        MOVUPS X0, 0x90(SP)
  main.go:8             0x48cf6a                488d050f190100          LEAQ 0x1190f(IP), AX
  main.go:8             0x48cf71                4889842490000000        MOVQ AX, 0x90(SP)
  main.go:8             0x48cf79                488d05a0cd0400          LEAQ 0x4cda0(IP), AX
  main.go:8             0x48cf80                4889842498000000        MOVQ AX, 0x98(SP)
  main.go:8             0x48cf88                c744243030000000        MOVL $0x30, 0x30(SP)
  main.go:8             0x48cf90                488d0561c80300          LEAQ 0x3c861(IP), AX
  main.go:8             0x48cf97                4889442448              MOVQ AX, 0x48(SP)
  main.go:8             0x48cf9c                488d842490000000        LEAQ 0x90(SP), AX
  main.go:8             0x48cfa4                4889442460              MOVQ AX, 0x60(SP)
  main.go:8             0x48cfa9                48c744246801000000      MOVQ $0x1, 0x68(SP)
  main.go:8             0x48cfb2                48c744247001000000      MOVQ $0x1, 0x70(SP)
  main.go:8             0x48cfbb                488d442430              LEAQ 0x30(SP), AX
  main.go:8             0x48cfc0                48890424                MOVQ AX, 0(SP)
  main.go:8             0x48cfc4                e867b7f9ff              CALL runtime.deferprocStack(SB)
  main.go:8             0x48cfc9                85c0                    TESTL AX, AX
  main.go:8             0x48cfcb                7516                    JNE 0x48cfe3
  main.go:9             0x48cfcd                90                      NOPL
  main.go:9             0x48cfce                e85dbdf9ff              CALL runtime.deferreturn(SB)
  main.go:9             0x48cfd3                488bac24a0000000        MOVQ 0xa0(SP), BP
  main.go:9             0x48cfdb                4881c4a8000000          ADDQ $0xa8, SP
  main.go:9             0x48cfe2                c3                      RET
  main.go:8             0x48cfe3                90                      NOPL
  main.go:8             0x48cfe4                e847bdf9ff              CALL runtime.deferreturn(SB)
  main.go:8             0x48cfe9                488bac24a0000000        MOVQ 0xa0(SP), BP
  main.go:8             0x48cff1                4881c4a8000000          ADDQ $0xa8, SP
  main.go:8             0x48cff8                c3                      RET
  main.go:7             0x48cff9                e8a247fcff              CALL runtime.morestack_noctxt(SB)
  main.go:7             0x48cffe                e92dffffff              JMP main.main(SB)

func deferprocStack(d *_defer), 这里将defer的函数调用全部放到g._defer上,串成一个链表等待调用,将新加入的defer调用放在前面,然后根据link去调用,这也能解释为什么越晚的defer越早调用.

func deferprocStack(d *_defer) {
    gp := getg()
    if gp.m.curg != gp {
        // go code on the system stack can't defer
        throw("defer on system stack")
    }
    // siz and fn are already set.
    // The other fields are junk on entry to deferprocStack and
    // are initialized here.
    d.started = false
    d.heap = false
    d.sp = getcallersp()
    d.pc = getcallerpc()
    // The lines below implement:
    //   d.panic = nil
    //   d.link = gp._defer
    //   gp._defer = d
    *(*uintptr)(unsafe.Pointer(&d._panic)) = 0
    *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
    *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
(dlv) p gp._defer
*runtime._defer {
        siz: 48,
        started: false,
        heap: false,
        sp: 824634305936,
        pc: 4771785,
        fn: *runtime.funcval {fn: 4745216},
        _panic: *runtime._panic nil,
        link: *runtime._defer {
                siz: 48,
                started: false,
                heap: false,
                sp: 824634306112,
                pc: 4772144,
                fn: *(*runtime.funcval)(0x4c97f8),
                _panic: *runtime._panic nil,
                link: *(*runtime._defer)(0xc00008eed0),},}

前面都是遇到defer就将其加到gp._defer链表中,deferreturn才是真正执行的时候.
这里gp._defer = d.link相当于取出了最后一个defer, 然后调用jmpdefer执行串成了一个链表,怎么区分多个函数的defer呢,这里就通过sp指针,判断caller中sp指针和defer当时的sp指针来判断.

在这里调用了freedefer(d),会将当前d放到pp.deferpool中,类似于p.cache,是defer的本地缓存,当然如果本地缓存满了,会将pp.deferpool的数据放一半到sched.deferpool
runtime.jmpdefer中,b

func deferreturn(arg0 uintptr) {
    gp := getg()
    d := gp._defer
    if d == nil {
        return
    }
    sp := getcallersp()
    if d.sp != sp {
        return
    }
switch d.siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
    default:
        memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
    }
    fn := d.fn
    d.fn = nil
    gp._defer = d.link
    freedefer(d)
    jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

在jmpdefer中,当执行完后,

// void jmpdefer(fv, sp);
// called from deferreturn.
// 1. grab stored LR for caller
// 2. sub 4 bytes to get back to BL deferreturn
// 3. BR to fn
(base) ➜  readsrc go tool objdump -s "runtime.jmpdefer" ./test
TEXT runtime.jmpdefer(SB) /usr/local/go/src/runtime/asm_amd64.s
  asm_amd64.s:587       0x452dc0                488b542408              MOVQ 0x8(SP), DX// 第一个参数,fn地址
  asm_amd64.s:588       0x452dc5                488b5c2410              MOVQ 0x10(SP), BX // 第二个参数arg0
  asm_amd64.s:589       0x452dca                488d63f8                LEAQ -0x8(BX), SP //call deferreturn时压入的caller IP指针
  asm_amd64.s:590       0x452dce                488b6c24f8              MOVQ -0x8(SP), BP // call的上一个地址,改为基址
  asm_amd64.s:591       0x452dd3                48832c2405              SUBQ $0x5, 0(SP) //减去call指令,即下一次要执行call deferreturn
  asm_amd64.s:592       0x452dd8                488b1a                  MOVQ 0(DX), BX // 压入fn函数
  asm_amd64.s:593       0x452ddb                ffe3                    JMP BX 跳转到fn函数执行,用JMP而不是CALL,因为是同一个函数里面

如果中途调用goexit终止,他会负责处理整个调用堆栈的延迟函数

func Goexit() {
gp := getg()
    for {
        d := gp._defer
        if d == nil {
            break
        }
        if d.started {
            if d._panic != nil {
                d._panic.aborted = true
                d._panic = nil
            }
            d.fn = nil
            gp._defer = d.link
            freedefer(d)
            continue
        }
        d.started = true
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
        if gp._defer != d {
            throw("bad defer entry in Goexit")
        }
        d._panic = nil
        d.fn = nil
        gp._defer = d.link
        freedefer(d)
        // Note: we ignore recovers here because Goexit isn't a panic
    }
}

性能

延迟调用远不是一个call指令那么简单,会涉及到对象分配,缓存和多次函数调用。 在性能要求比较高的场合,应该避免使用defer,go1.13测试的时候有3x的性能差距

BenchmarkNormal-12      100000000           11.2 ns/op
BenchmarkDefer-12       37844540            31.1 ns/op

panic

panic和defer的实现类似,也是放在gp._panic上面
如果recovered,那么会调用recover,recover会调用gogo(&gp.sched),否则defer结束后打印panic

func gopanic(e interface{}) {
    var p _panic
    p.arg = e
    p.link = gp._panic
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
    for {
        d := gp._defer
        if d == nil {
            break
        }
        if d.started { //如果已经开始了,则执行下一个
            continue
                }
        if p.recovered { //如果defer中执行了recovered,
                  mcall(recovery) //调用recover继续执行
        throw("recovery failed") // mcall should not return
        }
    preprintpanics(gp._panic)
    fatalpanic(gp._panic) // should not return
}

recover的实现是gorecover
调用后,判断gp._panic
如果不为nil, 且不是recovered状态,那么设置其p.recovered=true,改为已恢复状态
注意: 这里也通过p.argp指针和当前的调用指针比较来区分不同函数的panic。

func gorecover(argp uintptr) interface{} {
    // Must be in a function running as part of a deferred call during the panic.
    // Must be called from the topmost function of the call
    // (the function used in the defer statement).
    // p.argp is the argument pointer of that topmost deferred function call.
    // Compare against argp reported by caller.
    // If they match, the caller is the one who can recover.
    gp := getg()
    p := gp._panic
    if p != nil && !p.recovered && argp == uintptr(p.argp) {
        p.recovered = true
        return p.arg
    }
    return nil
}

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:darcyaf

查看原文:golang 源码剖析(7): 延迟defer

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

556 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传