Golang执行意外的堆内存分配



在基准测试时,我注意到堆内存分配令人惊讶。减少重现后,我最终得到以下内容:

// --- 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
(...)

话虽如此,我有两个问题: * 任何人都可以知道为什么那里有堆分配以及它是否是"预期的"? * 我怎样才能在调试会话中走得更远?将内存转储到十六进制具有不同的地址布局,go工具objdump将输出反汇编,这会破坏地址位置的内容

使用go工具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

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

在 Go 中,参数按值传递,通常在堆栈上;堆栈在函数结束时回收。但是,从memAllocRepro函数返回引用&values会使memAllocRepro中声明的values参数的生存期超过函数的末尾。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参数返回为valuesvalues在堆栈上返回。noAlloc2函数中没有对values的引用,因此没有转义。

noAlloc2values: 无分配


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
$ 

最新更新