混合打印和 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
逃逸到堆。
我在看关于围棋的讲座。 一个关于堆栈增长的例子:
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
逃逸到堆。