首页 > 解决方案 > 混合打印和 fmt.Println 和堆栈增长

问题描述

我正在看关于围棋的讲座。一个关于堆栈增长的例子:

package main

import "fmt"

const size = 1024

func main() {
    fmt.Println("Start")
    s:= "HELLO"
    stackCopy(&s, 0, [size]int{})
}

func stackCopy(s *string, c int, a [size]int){
    println("println: ", s, *s)
    //fmt.Println("fmt:     ", s, *s)
    c++
    if c == 10 {
    return
    }
    stackCopy(s, c, a)
}

仅使用 println 时,“s”的地址会发生变化(堆栈正在增长并且数据移动到其他地方):

Start
println:  0xc000107f58 HELLO
println:  0xc000107f58 HELLO
println:  0xc000117f58 HELLO
println:  0xc000117f58 HELLO
println:  0xc000117f58 HELLO
println:  0xc000117f58 HELLO
println:  0xc00019ff58 HELLO
println:  0xc00019ff58 HELLO
println:  0xc00019ff58 HELLO
println:  0xc00019ff58 HELLO

当我混合 println 和 fmt.Println 或只有“s”的 fmt.Println 地址不会改变时:

Start
println:  0xc00010a040 HELLO
fmt:      0xc00010a040 HELLO
println:  0xc00010a040 HELLO
fmt:      0xc00010a040 HELLO
println:  0xc00010a040 HELLO
fmt:      0xc00010a040 HELLO
println:  0xc00010a040 HELLO
fmt:      0xc00010a040 HELLO
println:  0xc00010a040 HELLO
fmt:      0xc00010a040 HELLO
println:  0xc00010a040 HELLO
fmt:      0xc00010a040 HELLO
println:  0xc00010a040 HELLO
fmt:      0xc00010a040 HELLO
println:  0xc00010a040 HELLO
fmt:      0xc00010a040 HELLO
println:  0xc00010a040 HELLO
fmt:      0xc00010a040 HELLO
println:  0xc00010a040 HELLO
fmt:      0xc00010a040 HELLO

为什么会这样?

标签: gopointersstack

解决方案


区别在于使用内置 println()函数和标准 libfmt.Println()中的函数。

println()是一个内置函数,编译器知道println()不保留传递给它的任何参数,因此传递给它的参数不会转义到堆。

fmt.Prinln()来自标准库,它作为您的任何自定义函数处理:编译器不假设它不保留传递的参数,因此您传递给它的参数可能会逃逸到堆,因此它们被分配在堆上而不是在堆栈上。

这是一个重要的区别。接下来导致偏差的原因是 Go 具有动态堆栈:它从小开始,如果需要,它可以增长。有关详细信息,请参阅Go 是否具有“无限调用堆栈”等价物?由于你向递归函数传递了相当大的参数stackCopy()(大小为 1024 的数组,元素类型int为 4 或 8 字节),初始的小堆栈将不足,将分配更大的堆栈,导致堆栈分配的变量被移动,因此他们的地址改变了。使用时不会发生这种情况fmt.Println(),因为编译器推断s可能会逃逸,因此它是在堆上分配的,并且增加堆栈不会导致移动s.

您可以使用这些-gcflags '-m'标志来打印编译器的转义分析。

在第一种情况下:

./play.go:8:13: inlining call to fmt.Println
./play.go:13:16: s does not escape
./play.go:8:14: "Start" escapes to heap
./play.go:8:13: []interface {} literal does not escape
<autogenerated>:1: .this does not escape

这里重要的是:s does not escape.

在第二种情况下:

./play.go:15:13: inlining call to fmt.Println
./play.go:8:13: inlining call to fmt.Println
./play.go:13:16: leaking param: s
./play.go:15:14: "fmt:     " escapes to heap
./play.go:15:30: *s escapes to heap
./play.go:15:13: []interface {} literal does not escape
./play.go:9:2: moved to heap: s
./play.go:8:14: "Start" escapes to heap
./play.go:8:13: []interface {} literal does not escape
<autogenerated>:1: .this does not escape

如您所见,编译器推断这s是一个泄漏参数,*s逃逸到堆。


推荐阅读