混合打印和 fmt.Println 以及堆栈增长

Mix print and fmt.Println and stack growing

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

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 或仅 fmt.Println “s” 的地址不变时:

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

为什么会这样?

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

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

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

这是一个重要的区别。接下来导致偏差的原因是 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 逃逸到堆。