Golang使用值传递而不是指针传递的测试
众所周知,Golang中应该尽可能的使用值传递,而不是指针传递,除非结构体非常大,原因有以下几个:
- 使用指针会使逃逸分析认为内存应该分配到堆上
- 分配在栈上会随着堆栈的回收而回收,分配在堆上会增加GC的压力(每次多扫描很多指针)
- 在栈上分配内存的性能和栈上内存拷贝的性能都比较好
其中第二点,特别是对于Slice,其中使用指针会大大增加GC的扫描压力,增加GC耗时。对于map来说,也同样不应该使用指针。
go 1.5版本之后,如果你使用的map的key和value中都不包含指针,那么GC会忽略这个map。
那么问题来了,既然我们在C++中习惯性的使用指针来减少拷贝,在Golang中会带来副作用,那这个副作用到底有多大呢?在多大的结构中使用指针才能抵消他带来的副作用呢?本文简单的测试下
待测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package xcas type item struct { i int } func newItem() item { return item{i: 1} } func newItemSlice() []item { t := make([]item, 1000) for i := 0; i < 1000; i++ { t[i] = item{i: 1} } return t } func newItemPtr() *item { return &item{i: 1} } func newItemPtrSlice() []*item { t := make([]*item, 1000) for i := 0; i < 1000; i++ { t[i] = &item{i: 1} } return t } |
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
func BenchmarkNewStruct(b *testing.B) { for i := 0; i < b.N; i++ { newItem() } } func BenchmarkNewStructSlice(b *testing.B) { for i := 0; i < b.N; i++ { newItemSlice() } } func BenchmarkNewPointer(b *testing.B) { for i := 0; i < b.N; i++ { newItemPtr() } } func BenchmarkNewPointerSlice(b *testing.B) { for i := 0; i < b.N; i++ { newItemPtrSlice() } } |
性能对比
为了防止内联和优化,先编译再测试。
1 2 |
go test -c -gcflags="-l -N" ./xcas.test -test.bench=New -test.benchmem |
测试时并未关本机上其他程序,所以也许会有些许偏差,不过应该影响不大。
1 2 3 4 5 6 7 8 |
goos: darwin goarch: amd64 pkg: xcas BenchmarkNewStruct-12 521985945 2.20 ns/op 0 B/op 0 allocs/op BenchmarkNewStructSlice-12 406569 2785 ns/op 8192 B/op 1 allocs/op BenchmarkNewPointer-12 86041179 14.2 ns/op 8 B/op 1 allocs/op BenchmarkNewPointerSlice-12 76563 15385 ns/op 16192 B/op 1001 allocs/op PASS |
可以看到对于简单的这个struct,拷贝一次的话,性能差距有10倍以上了。
抵消副作用
为了达到我们使用指针减少拷贝的目的,抵消GC和申请堆内存的耗时,必须构造一个包含很多元素的struct。而其中的元素也不能是string或者slice,因为他们其实数据都是建在堆上的。而string类型设计时是不可修改的,拷贝时只有在修改时才会重新申请内存。所以struct内的元素在测试时刨除这种类型的影响,我就暂时只想到了拿很多个int64来测试。
生成了一堆变量:
1 2 3 4 5 6 7 |
type item struct { i int64 i1 int64 i2 int64 ... i9 int64 } |
测试时有两个变量,一个是struct的大小,这里也就是int64的数量,另一个是拷贝的次数,需要调整几次分别测试。
Benchmark测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
func BenchmarkCopyStruct(b *testing.B) { for i := 0; i < b.N; i++ { a := newItem() for i := 0; i < 10; i++ { c := a _ = c.i } } } func BenchmarkCopyPointer(b *testing.B) { for i := 0; i < b.N; i++ { a := newItemPtr() for i := 0; i < 10; i++ { c := a _ = c.i } } } |
编译并测试:
1 2 |
go test -c -gcflags="-l -N" ./xcas.test -test.bench=Copy -test.benchmem |
测试结果
方便对比,建个表格
5*int64
首先是5个int64的struct对比,直接看图。可以看到拷贝那么多次也还是值拷贝效率高……
拷贝次数 | 5*int64 Struct | 5*int64 Pointer |
---|---|---|
5 | 11.7 | 30.4 |
10 | 19.2 | 36.3 |
15 | 27.1 | 41.6 |
20 | 37.6 | 47.3 |
30 | 51.8 | 59.7 |
40 | 80.6 | 91.2 |
60 | 103 | 117 |
100 | 165 | 172 |
10*int64
这么大一般就是平时用到的比较大的struct的大小了,可以看到还需要拷贝15次左右,指针才会效率更好。
拷贝次数 | 10*int64 Struct | 10*int64 Pointer |
---|---|---|
5 | 23.3 | 35 |
10 | 36 | 40.2 |
15 | 49 | 45.8 |
20 | 69.5 | 51 |
30 | 98.4 | 63.5 |
40 | 124 | 95.1 |
15*int64
拷贝次数 | 15*int64 Struct | 15*int64 Pointer |
---|---|---|
5 | 27.2 | 39.8 |
10 | 44.8 | 44.6 |
100*int64
拷贝次数 | 100*int64 Struct | 100*int64 Pointer |
---|---|---|
5 | 110 | 121 |
结果还是比较出乎我的意料,可以看到100个int64大小的struct,在平时正常使用时一般都小于5次拷贝,这样依旧是值拷贝略微更优。
这么看来似乎几乎根本不需要用到指针了。除非在某些必然会逃逸的对象(例如interface化了),或者希望函数内能修改其值的一些情况下,才会使用到指针传递。