4

在进行基准测试时,我注意到一个令人惊讶的堆内存分配。减少复制后,我得到以下结果:

// --- Repro file ---
func memAllocRepro(values []int) *[]int {

  for {
        break
    }

    return &values
}

// --- Benchmark file ---
func BenchmarkMemAlloc(b *testing.B) {

    values := []int{1, 2, 3, 4}

    for i := 0; i < b.N; i++ {
        memAllocRepro(values)
    }
}

这是基准输出:

BenchmarkMemAlloc-4     50000000            40.2 ns/op        32 B/op          1 allocs/op
PASS
ok      memalloc_debugging  2.113s
Success: Benchmarks passed.

现在有趣的是,如果我删除 for 循环,或者如果我直接返回切片而不是切片指针,则不再有堆分配:

// --- Repro file ---
func noAlloc1(values []int) *[]int {

    return &values // No alloc!
}

func noAlloc2(values []int) []int {
  for {
        break
    }

    return values // No alloc!
}

// --- Benchmark file ---
func BenchmarkNoAlloc(b *testing.B) {

    values := []int{1, 2, 3, 4}

    for i := 0; i < b.N; i++ {
        noAlloc1(values)
        noAlloc2(values)
    }

基准测试结果:

BenchmarkNoAlloc-4      300000000            4.20 ns/op        0 B/op          0 allocs/op
PASS
ok      memalloc_debugging  1.756s
Success: Benchmarks passed.

我发现这非常令人困惑,并通过 Delve 确认反汇编确实在 memAllocRepro 函数的开头进行了分配:

(dlv) disassemble
TEXT main.memAllocRepro(SB) memalloc_debugging/main.go
        main.go:10      0x44ce10        65488b0c2528000000      mov rcx, qword ptr gs:[0x28]
        main.go:10      0x44ce19        488b8900000000          mov rcx, qword ptr [rcx]
        main.go:10      0x44ce20        483b6110                cmp rsp, qword ptr [rcx+0x10]
        main.go:10      0x44ce24        7662                    jbe 0x44ce88
        main.go:10      0x44ce26        4883ec18                sub rsp, 0x18
        main.go:10      0x44ce2a        48896c2410              mov qword ptr [rsp+0x10], rbp
        main.go:10      0x44ce2f        488d6c2410              lea rbp, ptr [rsp+0x10]
        main.go:10      0x44ce34        488d0525880000          lea rax, ptr [rip+0x8825]
        main.go:10      0x44ce3b        48890424                mov qword ptr [rsp], rax
=>      main.go:10      0x44ce3f*       e8bcebfbff              call 0x40ba00 runtime.newobject

但我必须说,一旦我达到了这一点,我就不能轻易地进一步挖掘。我很确定通过查看 RAX 寄存器指向的结构至少可以知道分配了哪种类型,但这样做并不是很成功。很久没有读到这样的反汇编了。

(dlv) regs
   Rip = 0x000000000044ce3f
   Rsp = 0x000000c042039f30
   Rax = 0x0000000000455660
   (...)

说了这么多,我有 2 个问题: * 任何人都可以说出为什么会有堆分配,以及它是否是“预期的”?* 我怎么能在我的调试会话中走得更远?将内存转储到十六进制有不同的地址布局,go tool objdump 会输出反汇编,这会破坏地址位置的内容

使用 go tool objdump 进行全功能转储:

TEXT main.memAllocRepro(SB) memalloc_debugging/main.go
  main.go:10        0x44ce10        65488b0c2528000000  MOVQ GS:0x28, CX            
  main.go:10        0x44ce19        488b8900000000      MOVQ 0(CX), CX              
  main.go:10        0x44ce20        483b6110        CMPQ 0x10(CX), SP           
  main.go:10        0x44ce24        7662            JBE 0x44ce88                
  main.go:10        0x44ce26        4883ec18        SUBQ $0x18, SP              
  main.go:10        0x44ce2a        48896c2410      MOVQ BP, 0x10(SP)           
  main.go:10        0x44ce2f        488d6c2410      LEAQ 0x10(SP), BP           
  main.go:10        0x44ce34        488d0525880000      LEAQ runtime.types+34656(SB), AX    
  main.go:10        0x44ce3b        48890424        MOVQ AX, 0(SP)              
  main.go:10        0x44ce3f        e8bcebfbff      CALL runtime.newobject(SB)      
  main.go:10        0x44ce44        488b7c2408      MOVQ 0x8(SP), DI            
  main.go:10        0x44ce49        488b442428      MOVQ 0x28(SP), AX           
  main.go:10        0x44ce4e        48894708        MOVQ AX, 0x8(DI)            
  main.go:10        0x44ce52        488b442430      MOVQ 0x30(SP), AX           
  main.go:10        0x44ce57        48894710        MOVQ AX, 0x10(DI)           
  main.go:10        0x44ce5b        8b052ff60600        MOVL runtime.writeBarrier(SB), AX   
  main.go:10        0x44ce61        85c0            TESTL AX, AX                
  main.go:10        0x44ce63        7517            JNE 0x44ce7c                
  main.go:10        0x44ce65        488b442420      MOVQ 0x20(SP), AX           
  main.go:10        0x44ce6a        488907          MOVQ AX, 0(DI)              
  main.go:16        0x44ce6d        48897c2438      MOVQ DI, 0x38(SP)           
  main.go:16        0x44ce72        488b6c2410      MOVQ 0x10(SP), BP           
  main.go:16        0x44ce77        4883c418        ADDQ $0x18, SP              
  main.go:16        0x44ce7b        c3          RET                 
  main.go:16        0x44ce7c        488b442420      MOVQ 0x20(SP), AX           
  main.go:10        0x44ce81        e86aaaffff      CALL runtime.gcWriteBarrier(SB)     
  main.go:10        0x44ce86        ebe5            JMP 0x44ce6d                
  main.go:10        0x44ce88        e85385ffff      CALL runtime.morestack_noctxt(SB)   
  main.go:10        0x44ce8d        eb81            JMP main.memAllocRepro(SB)      
  :-1           0x44ce8f        cc          INT $0x3

反汇编 RAX 寄存器指向的内存:

(dlv) disassemble -a 0x0000000000455660 0x0000000000455860
        .:0     0x455660        1800                    sbb byte ptr [rax], al
        .:0     0x455662        0000                    add byte ptr [rax], al
        .:0     0x455664        0000                    add byte ptr [rax], al
        .:0     0x455666        0000                    add byte ptr [rax], al
        .:0     0x455668        0800                    or byte ptr [rax], al
        .:0     0x45566a        0000                    add byte ptr [rax], al
        .:0     0x45566c        0000                    add byte ptr [rax], al
        .:0     0x45566e        0000                    add byte ptr [rax], al
        .:0     0x455670        8e66f9                  mov fs, word ptr [rsi-0x7]
        .:0     0x455673        1b02                    sbb eax, dword ptr [rdx]
        .:0     0x455675        0808                    or byte ptr [rax], cl
        .:0     0x455677        17                      ?
        .:0     0x455678        60                      ?
        .:0     0x455679        0d4a000000              or eax, 0x4a
        .:0     0x45567e        0000                    add byte ptr [rax], al
        .:0     0x455680        c01f47                  rcr byte ptr [rdi], 0x47
        .:0     0x455683        0000                    add byte ptr [rax], al
        .:0     0x455685        0000                    add byte ptr [rax], al
        .:0     0x455687        0000                    add byte ptr [rax], al
        .:0     0x455689        0c00                    or al, 0x0
        .:0     0x45568b        004062                  add byte ptr [rax+0x62], al
        .:0     0x45568e        0000                    add byte ptr [rax], al
        .:0     0x455690        c0684500                shr byte ptr [rax+0x45], 0x0
4

1 回答 1

5

转义分析确定对值的任何引用是否会转义声明该值的函数。

在 Go 中,参数是按值传递的,通常在堆栈上;堆栈在函数结束时被回收。&values但是,从memAllocRepro函数返回引用会给出在函数结束后的生命周期中values声明的参数。memAllocRepro变量被values移动到堆中。

memAllocRepro::&values分配

./escape.go:3:6: cannot inline memAllocRepro: unhandled op FOR
./escape.go:7:9: &values escapes to heap
./escape.go:7:9:    from ~r1 (return) at ./escape.go:7:2
./escape.go:3:37: moved to heap: values

noAlloc1函数被内联在main函数中。如有必要,参数在函数values中声明并且不会从main函数中转义。

noAlloc1: &values: 没有分配

./escape.go:10:6: can inline noAlloc1 as: func([]int)*[]int{return &values}
./escape.go:23:10: inlining call to noAlloc1 func([]int)*[]int{return &values}

noAlloc2函数values参数返回values为. values在堆栈上返回。函数中没有引用valuesnoAlloc2因此没有转义。

noAlloc2: values: 没有分配


package main

func memAllocRepro(values []int) *[]int {
    for {
        break
    }
    return &values
}

func noAlloc1(values []int) *[]int {
    return &values
}

func noAlloc2(values []int) []int {
    for {
        break
    }
    return values
}

func main() {
    memAllocRepro(nil)
    noAlloc1(nil)
    noAlloc2(nil)
}

输出:

$ go build -a -gcflags='-m -m' escape.go
# command-line-arguments
./escape.go:3:6: cannot inline memAllocRepro: unhandled op FOR
./escape.go:10:6: can inline noAlloc1 as: func([]int) *[]int { return &values }
./escape.go:14:6: cannot inline noAlloc2: unhandled op FOR
./escape.go:21:6: cannot inline main: non-leaf function
./escape.go:23:10: inlining call to noAlloc1 func([]int) *[]int { return &values }
./escape.go:7:9: &values escapes to heap
./escape.go:7:9:    from ~r1 (return) at ./escape.go:7:2
./escape.go:3:37: moved to heap: values
./escape.go:11:9: &values escapes to heap
./escape.go:11:9:   from ~r1 (return) at ./escape.go:11:2
./escape.go:10:32: moved to heap: values
./escape.go:14:31: leaking param: values to result ~r1 level=0
./escape.go:14:31:  from ~r1 (return) at ./escape.go:18:2
./escape.go:23:10: main &values does not escape
$ 
于 2018-03-09T23:16:25.917 回答