原文地址:https://medium.com/a-journey-with-go
对于许多 Go 开发人员来说,用指针传递结构体来代替直接传递结构体(直接传递会copy一份结构体),能提高程序的性能。
为了理解使用指针而不是结构副本的影响,我们看一下两个用例。
密集的数据分配
让我们举一个简单的例子,说明何时要为其值共享结构体:
type S struct {
a, b, c int64
d, e, f string
g, h, i float64
}
这是一个可以通过复制或指针共享的基本结构:
func byCopy() S {
return S{
a: 1, b: 1, c: 1,
e: "foo", f: "foo",
g: 1.0, h: 1.0, i: 1.0,
}
}
func byPointer() *S {
return &S{
a: 1, b: 1, c: 1,
e: "foo", f: "foo",
g: 1.0, h: 1.0, i: 1.0,
}
}
基于这 2 种方法,我们现在可以编写 2 个基准测试,其中一个通过复制传递结构:
func BenchmarkMemoryStack(b *testing.B) {
var s S
f, err := os.Create("stack.out")
if err != nil {
panic(err)
}
defer f.Close()
err = trace.Start(f)
if err != nil {
panic(err)
}
for i := 0; i < b.N; i++ {
s = byCopy()
}
trace.Stop()
b.StopTimer()
_ = fmt.Sprintf("%v", s.a)
}
另一个非常相似,当它通过指针传递时:
func BenchmarkMemoryHeap(b *testing.B) {
var s *S
f, err := os.Create("heap.out")
if err != nil {
panic(err)
}
defer f.Close()
err = trace.Start(f)
if err != nil {
panic(err)
}
for i := 0; i < b.N; i++ {
s = byPointer()
}
trace.Stop()
b.StopTimer()
_ = fmt.Sprintf("%v", s.a)
}
让我们运行基准测试:
go test *.go -bench=BenchmarkMemoryHeap -benchmem -run=^$ -count=10 > heap.txt && benchstat heap.txt
go test *.go -bench=BenchmarkMemoryStack -benchmem -run=^$ -count=10 > stack.txt && benchstat stack.txt
第一个测试结果
name time/op
MemoryHeap-8 45.6ns ± 7%
name alloc/op
MemoryHeap-8 96.0B ± 0%
name allocs/op
MemoryHeap-8 1.00 ± 0%
goos: darwin
goarch: amd64
BenchmarkMemoryHeap-8 24628110 45.7 ns/op 96 B/op 1 allocs/op
BenchmarkMemoryHeap-8 25671453 45.4 ns/op 96 B/op 1 allocs/op
BenchmarkMemoryHeap-8 26333338 43.3 ns/op 96 B/op 1 allocs/op
BenchmarkMemoryHeap-8 28119717 48.6 ns/op 96 B/op 1 allocs/op
BenchmarkMemoryHeap-8 22179019 48.6 ns/op 96 B/op 1 allocs/op
BenchmarkMemoryHeap-8 27263764 43.8 ns/op 96 B/op 1 allocs/op
BenchmarkMemoryHeap-8 26222460 47.5 ns/op 96 B/op 1 allocs/op
BenchmarkMemoryHeap-8 22571803 44.9 ns/op 96 B/op 1 allocs/op
BenchmarkMemoryHeap-8 26250232 45.8 ns/op 96 B/op 1 allocs/op
BenchmarkMemoryHeap-8 27823093 42.7 ns/op 96 B/op 1 allocs/op
PASS
ok command-line-arguments 12.247s
第二个测试结果
name time/op
MemoryStack-8 7.67ns ± 5%
name alloc/op
MemoryStack-8 0.00B
name allocs/op
MemoryStack-8 0.00
goos: darwin
goarch: amd64
BenchmarkMemoryStack-8 157662150 7.83 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryStack-8 152284056 7.51 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryStack-8 159412387 7.67 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryStack-8 143585290 9.64 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryStack-8 155647315 7.64 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryStack-8 158821363 7.57 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryStack-8 133672618 8.04 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryStack-8 154818850 7.58 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryStack-8 157884108 7.70 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryStack-8 135765003 7.52 ns/op 0 B/op 0 allocs/op
PASS
ok command-line-arguments 19.972s
从结果来看第二种比第一种快了将近7倍,不是说指针传递会更快吗。从测试结果来看,主要是因为在堆上分配内存,导致更长的耗时。
在继续看trace的测试分析
go tool trace stack.out
很明显stack Heap行数据只有开始的时候有一点,这应该是测试代码自带的一些内存分配。而通过值返回结构体并不会在堆上分配内存。也不会有goroutine去回收内存
go tool trace heap.out
对于第二个图,指针的使用迫使 go 编译器将变量分配到堆中,并触发了多次的垃圾回收。 如果我们放大图表,我们可以看到垃圾回收占据了进程的重要部分。
可以看出使用指针的话会导致GC和内存分配,这是最终导致测试结果比较慢的原因。
蓝色、粉色和红色是垃圾收集器的阶段,而棕色是与堆上的分配相关的(图中用“runtime.bgsweep”标记):
即使这个例子有点极端,我们也可以看到在堆上而不是在堆栈上分配一个变量是多么昂贵。 在我们的示例中,代码在堆栈上分配结构并复制它比在堆上分配它并共享其地址要快得多。
如果我们将处理器限制为 1 且 GOMAXPROCS=1,情况可能会更糟:
stack:
name time/op
MemoryStack-8 7.71ns ± 1%
name alloc/op
MemoryStack-8 0.00B
name allocs/op
MemoryStack-8 0.00
---
heap:
name time/op
MemoryHeap-8 80.5ns ± 2%
name alloc/op
MemoryHeap-8 96.0B ± 0%
name allocs/op
MemoryHeap-8 1.00 ± 0%
差距变成了10倍
密集的函数调用
对于第二个用例,我们将向我们的结构添加两个空方法,并对我们的基准测试稍作调整:
func (s S) stack(s1 S) {}
func (s *S) heap(s1 *S) {}
在堆栈上分配的基准测试将创建一个结构并通过复制传递它:
func BenchmarkMemoryStackFunc(b *testing.B) {
var s S
var s1 S
s = byCopy()
s1 = byCopy()
for i := 0; i < b.N; i++ {
for j := 0; j < 1000000; j++ {
s.stack(s1)
}
}
}
func BenchmarkMemoryHeapFunc(b *testing.B) {
var s *S
var s1 *S
s = byPointer()
s1 = byPointer()
for i := 0; i < b.N; i++ {
for j := 0; j < 1000000; j++ {
s.heap(s1)
}
}
}
正如预期的那样,通过指针传递参数会更快:
name time/op
MemoryStackFunc-8 548µs ± 2%
name alloc/op
MemoryStackFunc-8 0.00B
name allocs/op
MemoryStackFunc-8 0.00
------
name time/op
MemoryHeapFunc-8 278µs ± 2%
name alloc/op
MemoryHeapFunc-8 0.00B
name allocs/op
MemoryHeapFunc-8 0.00
总结
在 go 中使用指针而不是结构体的副本并不总是一件好事。在频繁申请变量的地方可以使用结构体副本,而传递参数时可以使用指针。