Go返回指针的坏处
作为一个不再年轻的C程序员,我苦恼于一点:函数返回结构体的指针是完全正常的。但我感觉这是Go的坏处,通常我认为返回结构体的值会更好。我试着证明返回结构体的值会更好。
我定义一个可以很容易改变大小 的结构体。结构体的内容是一个数组:通过改变数组的大小来简单地改变结构体的大小。
|
|
接下来,我将创建两个程序来构建这个结构体的新版本。一个以指针将它返回,另一个返回它的值。
|
|
最后,我写两个基准测试来测试获取和使用这些结构体的耗时。我在结构体中的值做些简单的计算,以便让编译器不会仅仅优化整个过程。
|
|
将 bigStructSize
设置为10,按值返回将是按指针返回的两倍。在指针示例中,内存被分配到堆上,这将花费大约20ns,然后数据被设置(在两个示例中花费的时间应该是一样的),再然后指针被写入到栈中,返回结构体给调用者。在值示例中,没有内存分配,但是整个结构体必须被拷贝到栈上并返回给它的调用者。
在这种结构体大小下,在栈上复制数据的开销小于分配内存的开销。
|
|
当修改 bigStructSize
为 100, 现在结构体包含 100个整型,以绝对值表示的差距将增加 – 尽管指针示例中增加的百分比比较少。
|
|
如果我们将结构体改为1000个整型,返回指针是否会更快?
BenchmarkStructReturnValue-8 2000000 830 ns/op 0 B/op 0 allocs/op
BenchmarkStructReturnPointer-8 1000000 1401 ns/op 8192 B/op 1 allocs/op
不,情况更糟,那增加到10000呢?
BenchmarkStructReturnValue-8 100000 13332 ns/op 0 B/op 0 allocs/op
BenchmarkStructReturnPointer-8 200000 11032 ns/op 81920 B/op 1 allocs/op
最后,在结构体中包含10000个整型下,结构体返回指针更快。经过进一步的调查,看来我的笔记本电脑上的临界点是2700。在这一点上,我几乎不知道为什么1000 整型会有如此大的差异。让我们看下基准的 profile !
go test -bench BenchmarkStructReturnValue -run ^$ -cpuprofile cpu2.prof
go tool pprof post.test cpu2.prof
(pprof) top
Showing nodes accounting for 2.25s, 100% of 2.25s total
flat flat% sum% cum cum%
2.09s 92.89% 92.89% 2.23s 99.11% github.com/philpearl/blog/content/post.newBigStruct
0.14s 6.22% 99.11% 0.14s 6.22% runtime.newstack
0.02s 0.89% 100% 0.02s 0.89% runtime.nanotime
0 0% 100% 2.23s 99.11% github.com/philpearl/blog/content/post.BenchmarkStructReturnValue
0 0% 100% 0.02s 0.89% runtime.mstart
0 0% 100% 0.02s 0.89% runtime.mstart1
0 0% 100% 0.02s 0.89% runtime.sysmon
0 0% 100% 2.23s 99.11% testing.(*B).launch
0 0% 100% 2.23s 99.11% testing.(*B).runN
在值返回的示例中,几乎所有的工作发生在 newBigStruct
。这一切是相关的简单,如果我们看指针示例的 profile 会发生什么?
go test -bench BenchmarkStructReturnPointer -run ^$ -cpuprofile cpu.prof
go tool pprof post.test cpu.prof
(pprof) top
Showing nodes accounting for 2690ms, 93.08% of 2890ms total
Dropped 28 nodes (cum <= 14.45ms)
Showing top 10 nodes out of 67
flat flat% sum% cum cum%
1110ms 38.41% 38.41% 1110ms 38.41% runtime.pthread_cond_signal
790ms 27.34% 65.74% 790ms 27.34% runtime.pthread_cond_wait
300ms 10.38% 76.12% 300ms 10.38% runtime.usleep
200ms 6.92% 83.04% 200ms 6.92% runtime.pthread_cond_timedwait_relative_np
80ms 2.77% 85.81% 80ms 2.77% runtime.nanotime
60ms 2.08% 87.89% 140ms 4.84% runtime.sweepone
50ms 1.73% 89.62% 50ms 1.73% runtime.pthread_mutex_lock
40ms 1.38% 91.00% 150ms 5.19% github.com/philpearl/blog/content/post.newBigStructPtr
30ms 1.04% 92.04% 40ms 1.38% runtime.gcMarkDone
30ms 1.04% 93.08% 40ms 1.38% runtime.scanobject
在 newBigStructPtr
示例中,情况要复杂的多,有很多函数使用大量的CPU。在 newBigStructPtr
的设置只花费了大约 5% 的时间。相反,在Go运行时,有很多的时间花费在处理线程,锁和垃圾回收。底层的函数返回指针很快,但是分配指针所带来的负担却是巨大的开销。
现在,这种情况非常简单,数据被创建后立即被丢弃,因此垃圾收集器将承受巨大的负担。如果返回的数据的生存期更长,则结果可能会大不相同。但这也许表明,返回具有较短生存期结构体指针是不好的。