返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

6.1 defer

发布于 2024-10-12 19:16:07 字数 12075 浏览 0 评论 0 收藏 0

延迟调用通常被编译器转换为 deferproc 和 deferreturn 调用。

1.13: deferprocStack.

1.14: calling the deferred function directly. (inline)

deferproc

创建 defer 对象,保存延迟函数执行数据。

  lo  +------------+  +0 SP
      | siz        |
      +------------+  +8
      | fn         |
      +------------+  +10  <--- argp
      | arg0       |
      +------------+
      | ...        |
  hi  +------------+
// panic.go

// Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this.

func deferproc(siz int32, fn *funcval) {   // arguments of fn follow fn
    gp := getg()
 
    // 获取 caller sp、pc,确认参数地址。
    sp := getcallersp()
    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    callerpc := getcallerpc()
    
    // 创建 defer,保存相关信息。
    d := newdefer(siz)
    
	d.link = gp._defer
	gp._defer = d
    
    d.fn = fn
    d.pc = callerpc
    d.sp = sp
    
    // 复制调用参数。
    switch siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
    default:
        memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
    }
    
    // deferproc returns 0 normally.
    // a deferred func that stops a panic makes the deferproc return 1.
    // the code the compiler generates always checks the return value and 
    // jumps to the end of the function if deferproc returns != 0.
    
    return0()
    
    // No code can go here - the C return register has
   // been set and must not be clobbered.
}

同样按等级,提供二级缓存复用方式。

垃圾回收 gcStart 会调用 clearpools 清理全局缓存。

// runtime2.go

type _defer struct {
    siz     int32
    started bool
    heap    bool
    
    sp      uintptr    // sp at time of defer
    pc      uintptr
    fn      *funcval
    
    _panic  *_panic    // panic that is running defer
    link    *_defer
}
// runtime2.go

type p struct {
    deferpool  [5][]*_defer
}

type schedt struct {
    deferpool  [5]*_defer
}

优先从本地获取。如本地已空,则从全局转移一批到本地。

// panic.go

// Allocate a Defer, usually using per-P pool.
// Each defer must be released with freedefer.

func newdefer(siz int32) *_defer {
    var d *_defer
    
    // 按参数长度获取等级。
    sc := deferclass(uintptr(siz))
    
    // 从缓存获取。
    gp := getg()
    
    if sc < uintptr(len(p{}.deferpool)) {
        pp := gp.m.p.ptr()
        
        // 如果本地缓存已空,则从全局提取一批到本地。
        if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
            systemstack(func() {
                for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
                    d := sched.deferpool[sc]
                    sched.deferpool[sc] = d.link
                    d.link = nil
                    pp.deferpool[sc] = append(pp.deferpool[sc], d)
                }
            })
        }
        
        // 从本地缓存提取。
        if n := len(pp.deferpool[sc]); n > 0 {
            d = pp.deferpool[sc][n-1]
            pp.deferpool[sc][n-1] = nil
            pp.deferpool[sc] = pp.deferpool[sc][:n-1]
        }
    }
    
    // 直接从堆分配。(没有复用缓存,或超出大小等级)
    if d == nil {
        // Allocate new defer+args.
        systemstack(func() {
            total := roundupsize(totaldefersize(uintptr(siz)))
            d = (*_defer)(mallocgc(total, deferType, true))
        })
    }
    
    // 存入 G._defer 链表。
    d.siz = siz
    d.heap = true
    
    return d
}

所有延迟调用(defer)都保存到 `G._defer`,等待 deferreturn 执行。

// runtime2.go

type g struct {
    _defer  *_defer
}

所分配内存含头和参数。

// total size of memory block for defer with arg size sz

func totaldefersize(siz uintptr) uintptr {
    if siz <= minDeferArgs {
        return minDeferAlloc
    }
    return deferHeaderSize + siz
}

const deferHeaderSize = unsafe.Sizeof(_defer{})

deferprocStack

相比堆分配,直接在栈内存储 defer 对象,无疑可提升性能。

d.heap = false

// deferprocStack queues a new deferred function with a defer record on the stack.
// The defer record must have its siz and fn fields initialized.
// The defer record must be immediately followed in memory by the arguments of the defer.

func deferprocStack(d *_defer) {
    gp := getg()
    
    // 参数设置,其中 siz、fn 由外部设置。
    d.started = false
    d.heap = false          // !!!!!!!
    d.sp = getcallersp()
    d.pc = getcallerpc()
    
    // 添加到 G._defer 链表。
    *(*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))
    
    return0()
}
func freedefer(d *_defer) {
    if !d.heap {
        return
    }
}

deferreturn

从 G 链表找到当前函数(caller)所属 defer,拷贝参数到栈,执行。

这个栈是 caller G.stack 内存。

按调用约定,参数 arg0 对应 caller.SP。

稍后用该位置(SP)存储调用参数,并不关心它(arg0)的实际内容。

从 deferArgs 复制调用参数到到栈(SP),调用(jmpdefer)执行延迟函数。

前面调用 deferproc 时,存过同样参数,故无须担心内存不足。

// Run a deferred function if there is one.
// The compiler inserts a call to this at the end of any
// function which calls defer.

func deferreturn(arg0 uintptr) {
 
    // 从 G._defer 链表提取。
    gp := getg()
    d := gp._defer
    if d == nil {
        return
    }
    
    // deferproc 和 deferreturn 由同一 caller 调用。
    // 对比 caller sp 值,可知道 G._defer 链上哪些属于当前 caller。
    sp := getcallersp()
    if d.sp != sp {
        return
    }
    
    // 将参数复制到栈(caller SP)。
    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 前,堆栈状态。

               +----------------+   
  deferreturn  | fn             |     执行 jmpdefer 所需参数。
               +----------------+
               | &arg0          |
               +----------------+
               | ...            |
               +----------------+
               | caller.BP      |
           ----+----------------+
               | caller.IP      |
           ----+----------------+ SP
               | arg0           |     延迟调用参数。
       caller  +----------------+
               |                |
// asm_amd64.s

// func jmpdefer(fv *funcval, argp uintptr)
// argp is a caller SP.
// called from deferreturn.
// 1. pop the caller
// 2. sub 5 bytes from the callers return
// 3. jmp to the argument

TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
    MOVQ    fv+0(FP), DX          // fv  : fn
    MOVQ    argp+8(FP), BX        // argp: caller SP
    LEAQ    -8(BX), SP            // SP  : caller.SP - 8 ---> caller.IP
    MOVQ    -8(SP), BP            // BP  : caller.BP
    SUBQ    $5, (SP)              // caller.IP -= 5
    MOVQ    0(DX), BX             // jmp fn
    JMP     BX

调用后,堆栈状态。

               |                |
           ----+----------------+
               | deferreturn.IP |       DX = fn, BX = &arg0
           ----+----------------+
               | fv: fn         | 
               +----------------+    
  deferreturn  | argp: &arg0    |  
               +----------------+
               | ...            |
               +----------------+    
               | caller.BP      | 
           ----+----------------+ SP ---> 延迟函数(fn)将从此处开始执行 !!!
               | caller.IP - 5  |
           ----+----------------+ 
               | arg0           |  
       caller  +----------------+
               | ...            |
               +----------------+ BP

1. 将 fn 和 arg0 地址分别保存到 DX 和 BX。

2. 透过 BX - 8 ,将 SP 指向 caller.IP 存储位置。

3. 透过 SP - 8 ,将 deferreturn 保存的 caller.BP 存入 BP。

4. 通过 SP,将保存的 caller.IP - 5

5. 执行延迟函数(JMP,不会有 PC/IP 入栈)。

CALL deferreturn 指令长度 5 字节,调用该指令后 IP += 5

caller.IP - 5 ,重新指向 CALL deferreturn 指令。

如此,就可多次调用 deferreturn,遍历执行 G._defer 链中的延迟函数。

所以说,fn 覆盖 deferreturn 栈帧,类似 POP deferreturn 操作。

最终效果,由 caller 直接 CALL fn(arg0) 假象。

延迟函数保存的 BP,是 caller.BP。

延迟函数 RET 的 IP,是 caller.IP。

示例

不同版本编译结果。

package main

//go:noinline
func test(x int) {
	println(x)
}

func main() {
	defer test(100)
}

1.13: 在栈上分配 _defer 对象,然后将其地址传递给 deferprocStack。

$ go build -gcflags -S

"".main STEXT size=115 args=0x0 locals=0x48
  0x000f 00015 (test.go:8)  SUBQ  $72, SP
  0x0013 00019 (test.go:8)  MOVQ  BP, 64(SP)
  0x0018 00024 (test.go:8)  LEAQ  64(SP), BP
  
  0x001d 00029 (test.go:9)  MOVL  $8, ""..autotmp_0+8(SP)      SP +-------------+ 0
  0x0025 00037 (test.go:9)  LEAQ  "".test·f(SB), AX               | arg_d: &d   |
  0x002c 00044 (test.go:9)  MOVQ  AX, ""..autotmp_0+32(SP)        +-------------+ 8   --+
  0x0031 00049 (test.go:9)  MOVQ  $100, ""..autotmp_0+56(SP)      | siz: 8, ... |       |
  0x003a 00058 (test.go:9)  LEAQ  ""..autotmp_0+8(SP), AX         +-------------+ 16    |
  0x003f 00063 (test.go:9)  MOVQ  AX, (SP)                        | sp          |       |     
  0x0043 00067 (test.go:9)  CALL  runtime.deferprocStack(SB)      +-------------+ 24    |
  0x0048 00072 (test.go:9)  TESTL AX, AX                          | pc          |       |
  0x004a 00074 (test.go:9)  JNE 92                                +-------------+ 32    +--> d: _defer
                                                                  | fn: test.f  |       |
  0x004c 00076 (test.go:10) XCHGL AX, AX                          +-------------+ 40    |
  0x004d 00077 (test.go:10) CALL  runtime.deferreturn(SB)         | _panic      |       |
  0x0052 00082 (test.go:10) MOVQ  64(SP), BP                      +-------------+ 48    |
  0x0057 00087 (test.go:10) ADDQ  $72, SP                         | _link       |       |
  0x005b 00091 (test.go:10) RET                                   +-------------+     --+
  0x005c 00092 (test.go:9)  XCHGL AX, AX                          1.13 runtime2.go
  0x005d 00093 (test.go:9)  CALL  runtime.deferreturn(SB)
  
  0x0062 00098 (test.go:9)  MOVQ  64(SP), BP
  0x0067 00103 (test.go:9)  ADDQ  $72, SP
  0x006b 00107 (test.go:9)  RET

1.14: 直接调用。

并不总是有效。比如放到循环内,就退回到 deferproc。

"".main STEXT size=118 args=0x0 locals=0x28
  0x000f 00015 (test.go:8)  SUBQ  $40, SP
  0x0013 00019 (test.go:8)  MOVQ  BP, 32(SP)
  0x0018 00024 (test.go:8)  LEAQ  32(SP), BP
  
  0x001d 00029 (test.go:8)  MOVQ  $0, AX                      SP +----------+ 0
  0x0024 00036 (test.go:8)  MOVQ  AX, 24(SP)                     |          |
  0x0029 00041 (test.go:8)  MOVB  $0, ""..autotmp_0+15(SP)       +----------+ 8
  0x002e 00046 (test.go:9)  LEAQ  "".test·f(SB), AX              |          |
  0x0035 00053 (test.go:9)  MOVQ  AX, ""..autotmp_1+24(SP)       +----------+ 16
  0x003a 00058 (test.go:9)  MOVQ  $100, ""..autotmp_2+16(SP)     | 100      |
  0x0043 00067 (test.go:10) MOVB  $0, ""..autotmp_0+15(SP)       +----------+ 24
                                                                 | test.f   |
  0x0048 00072 (test.go:10) MOVQ  ""..autotmp_2+16(SP), AX       +----------+ 32
  0x004d 00077 (test.go:10) MOVQ  AX, (SP)
  0x0051 00081 (test.go:10) CALL  "".test(SB)
  
  0x0056 00086 (test.go:10) MOVQ  32(SP), BP
  0x005b 00091 (test.go:10) ADDQ  $40, SP
  0x005f 00095 (test.go:10) RET
  
  0x0060 00096 (test.go:10) CALL  runtime.deferreturn(SB)
  0x0065 00101 (test.go:10) MOVQ  32(SP), BP
  0x006a 00106 (test.go:10) ADDQ  $40, SP
  0x006e 00110 (test.go:10) RET

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文