内容目录
对于许多Go开发人员而言,就性能而言,系统地使用指针共享结构体而不是副本本身似乎是最佳选择。
为了理解使用指针而不是结构体副本的影响,我们将回顾两个用例。
密集分配数据
以下面结构体为例
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 ./... -bench=BenchmarkMemoryHeap -benchmem -run=^$ -count=10 > head.txt && benchstat head.txt go test ./... -bench=BenchmarkMemoryStack -benchmem -run=^$ -count=10 > stack.txt && benchstat stack.txt
测试结果如下
name time/op MemoryHeap-4 75.0ns ± 5% name alloc/op MemoryHeap-4 96.0B ± 0% name allocs/op MemoryHeap-4 1.00 ± 0% ------------------ name time/op MemoryStack-4 8.93ns ± 4% name alloc/op MemoryStack-4 0.00B name allocs/op MemoryStack-4 0.00
这里使用副本比指针快8倍。
为了理解,让我们看看基于trace生成的图:
第一张图非常简单。因为它没有使用堆,也就没有gc和其他额外的goroutine。
对于第二张图,指针的使用迫使go编译器将变量逃逸到堆中并产生gc压力。如果放大图,我们可以发现gc占据了进程的重要部分:
我们可以发现gc每4ms执行一次。
如果我们再次放大,我们可以发现更多细节:
蓝色,粉红色和红色是垃圾收集器的阶段,而棕色的与堆上的分配有关(在图中用“ runtime.bgsweep”标记):
即使这个例子有点极端,我们也可以看到在堆而不是栈上分配变量的代价是多么昂贵。 在我们的示例中,在栈上复制结构体比堆上共享指针快的多。
如果我们限制处理器为1(GOMAXPROCS=1),情况将会更糟:
name time/op MemoryHeap 114ns ± 4% name alloc/op MemoryHeap 96.0B ± 0% name allocs/op MemoryHeap 1.00 ± 0% ------------------ name time/op MemoryStack 8.77ns ± 5% name alloc/op MemoryStack 0.00B name allocs/op MemoryStack 0.00
堆分配测试从75ns/op 降低到 114ns/op。
密集方法调用
对于第二个用例,我们将向结构中添加两个空方法:
func (s S) stack(s1 S) {} func (s *S) heap(s1 *S) {}
使用副本方式调用:
func BenchmarkMemoryStack(b *testing.B) { var s S var s1 S s = byCopy() s1 = byCopy() for i := 0; i < b.N; i++ { for i := 0; i < 1000000; i++ { s.stack(s1) } } }
使用指针方式调用:
func BenchmarkMemoryHeap(b *testing.B) { var s *S var s1 *S s = byPointer() s1 = byPointer() for i := 0; i < b.N; i++ { for i := 0; i < 1000000; i++ { s.heap(s1) } } }
和预期大相径庭的结果:
name time/op MemoryHeap-4 301µs ± 4% name alloc/op MemoryHeap-4 0.00B name allocs/op MemoryHeap-4 0.00 ------------------ name time/op MemoryStack-4 595µs ± 2% name alloc/op MemoryStack-4 0.00B name allocs/op MemoryStack-4 0.00
结论
在go中使用指针而不是结构的副本并不总是件好事。
本文编译自 Go: Should I Use a Pointer instead of a Copy of my Struct?