写点什么

Go: 应该使用指针还是结构体副本?

用户头像
陈思敏捷
关注
发布于: 2020 年 05 月 23 日
Go: 应该使用指针还是结构体副本?

对于许多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?

博客地址:https://www.chenjie.info/2385



本文首发于我的公众号:



发布于: 2020 年 05 月 23 日阅读数: 72
用户头像

陈思敏捷

关注

多动脑不痴呆 2017.12.21 加入

gopher

评论

发布
暂无评论
Go: 应该使用指针还是结构体副本?