为什么 go runtime.newstack 分配比在堆上分配相同大小的变量便宜得多?
Why go runtime.newstack allocation is much more cheaper than allocating same size variable on the heap?
我正在尝试了解 Go 的一些内部结构。但是我无法理解的一件事是 goroutines 的堆栈如何增加。
我看到 go 使用 runtime.morestack
分配新堆栈,然后将其复制到新创建的区域。
虽然,它与堆分配有何不同?
更准确地说:为什么函数 stack()
的运行速度几乎是函数 heap()
的十倍?
func stack() {
_ = stackit()
}
//go:noinline
func stackit() [8000]byte {
return [8000]byte{}
}
func heap() {
_ = heapit()
}
//go:noinline
func heapit() *[8000]byte {
a := [8000]byte{}
return &a
}
基准:
❯ go test -bench=. -benchmem
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkStackIt-12 11208864 103.9 ns/op 0 B/op 0 allocs/op
BenchmarkHeapIt-12 1309708 919.5 ns/op 8192 B/op 1 allocs/op
PASS
ok _/Users/asnelzin/dev/experiment/lstack 3.981s
如您所见,第一个函数中没有堆分配,但是 g.stack 应该扩大以适合字节数组。
首先,正如@kostix 所说,只有一个堆栈在增长,因此基准测试不是这些测量的正确工具。
我尝试测量 stack()
和 heap()
函数的单次调用,但仍然得到意想不到的结果:115ns vs 15us.
所以我从头开始,找出真正优化我的代码并完全删除堆栈分配的编译器。有 stackit()
的调用,但没有堆栈分配。
我重写了示例并添加了打印以查找堆栈增长的时刻。
package main
import (
"fmt"
"time"
)
const size = 256
func main() {
print("main\n")
start := time.Now()
stack()
fmt.Println(time.Since(start))
}
//go:noinline
func stack() {
print("stack\n")
x := [size]int64{}
end(x)
}
//go:noinline
func heap() {
print("heap\n")
_ = heapit()
}
//go:noinline
func end(x [size]int64) {
_ = x
}
//go:noinline
func heapit() *[size]int64 {
return &[size]int64{}
}
运行 这个用 stackDebug = 1
编译的 go 版本给出了这个输出:
<...>
main
runtime: newstack sp=0xc000070f10 stack=[0xc000070000, 0xc000071000]
morebuf={pc:0x10a2dcc sp:0xc000070f20 lr:0x0}
sched={pc:0x10a2f09 sp:0xc000070f18 lr:0x0 ctxt:0x0}
stackalloc 16384
stackcacherefill order=3
allocated 0xc00010a000
copystack gp=0xc000000180 [0xc000070000 0xc000070f18 0xc000071000] -> [0xc00010a000 0xc00010df18 0xc00010e000]/16384
stackfree 0xc000070000 4096
stack grow done
stack
50.096µs
现在显然堆栈增长到 16384 字节。这里重要的部分是复制旧的堆栈到新的内存区域。
堆版本不会改变堆栈的大小:
main
heap
6.096µs
这个数字似乎不公平,所以我再次检查了堆分配和 运行 基准测试:
转义易观结果:
> go build -gcflags '-m -l' -o sstack ./main.go
./main.go:37:9: &[256]int64{} escapes to heap
<...>
基准测试结果:
> go test -bench=. -benchmem
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkStackIt-12 24075922 43.80 ns/op 0 B/op 0 allocs/op
BenchmarkHeapIt-12 5042449 236.8 ns/op 2048 B/op 1 allocs/op
PASS
ok _/Users/asnelzin/dev/experiment/sstack 2.804s
所以是的,创建新堆栈并将旧堆栈的内容复制到新创建的堆栈会产生开销。但是在长期 运行 上,这个开销与堆分配的开销相比相形见绌。
我正在尝试了解 Go 的一些内部结构。但是我无法理解的一件事是 goroutines 的堆栈如何增加。
我看到 go 使用 runtime.morestack
分配新堆栈,然后将其复制到新创建的区域。
虽然,它与堆分配有何不同?
更准确地说:为什么函数 stack()
的运行速度几乎是函数 heap()
的十倍?
func stack() {
_ = stackit()
}
//go:noinline
func stackit() [8000]byte {
return [8000]byte{}
}
func heap() {
_ = heapit()
}
//go:noinline
func heapit() *[8000]byte {
a := [8000]byte{}
return &a
}
基准:
❯ go test -bench=. -benchmem
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkStackIt-12 11208864 103.9 ns/op 0 B/op 0 allocs/op
BenchmarkHeapIt-12 1309708 919.5 ns/op 8192 B/op 1 allocs/op
PASS
ok _/Users/asnelzin/dev/experiment/lstack 3.981s
如您所见,第一个函数中没有堆分配,但是 g.stack 应该扩大以适合字节数组。
首先,正如@kostix 所说,只有一个堆栈在增长,因此基准测试不是这些测量的正确工具。
我尝试测量 stack()
和 heap()
函数的单次调用,但仍然得到意想不到的结果:115ns vs 15us.
所以我从头开始,找出真正优化我的代码并完全删除堆栈分配的编译器。有 stackit()
的调用,但没有堆栈分配。
我重写了示例并添加了打印以查找堆栈增长的时刻。
package main
import (
"fmt"
"time"
)
const size = 256
func main() {
print("main\n")
start := time.Now()
stack()
fmt.Println(time.Since(start))
}
//go:noinline
func stack() {
print("stack\n")
x := [size]int64{}
end(x)
}
//go:noinline
func heap() {
print("heap\n")
_ = heapit()
}
//go:noinline
func end(x [size]int64) {
_ = x
}
//go:noinline
func heapit() *[size]int64 {
return &[size]int64{}
}
运行 这个用 stackDebug = 1
编译的 go 版本给出了这个输出:
<...>
main
runtime: newstack sp=0xc000070f10 stack=[0xc000070000, 0xc000071000]
morebuf={pc:0x10a2dcc sp:0xc000070f20 lr:0x0}
sched={pc:0x10a2f09 sp:0xc000070f18 lr:0x0 ctxt:0x0}
stackalloc 16384
stackcacherefill order=3
allocated 0xc00010a000
copystack gp=0xc000000180 [0xc000070000 0xc000070f18 0xc000071000] -> [0xc00010a000 0xc00010df18 0xc00010e000]/16384
stackfree 0xc000070000 4096
stack grow done
stack
50.096µs
现在显然堆栈增长到 16384 字节。这里重要的部分是复制旧的堆栈到新的内存区域。
堆版本不会改变堆栈的大小:
main
heap
6.096µs
这个数字似乎不公平,所以我再次检查了堆分配和 运行 基准测试:
转义易观结果:
> go build -gcflags '-m -l' -o sstack ./main.go
./main.go:37:9: &[256]int64{} escapes to heap
<...>
基准测试结果:
> go test -bench=. -benchmem
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkStackIt-12 24075922 43.80 ns/op 0 B/op 0 allocs/op
BenchmarkHeapIt-12 5042449 236.8 ns/op 2048 B/op 1 allocs/op
PASS
ok _/Users/asnelzin/dev/experiment/sstack 2.804s
所以是的,创建新堆栈并将旧堆栈的内容复制到新创建的堆栈会产生开销。但是在长期 运行 上,这个开销与堆分配的开销相比相形见绌。