1.问题
调用一个函数,如果入参是一个 struct,我们应该使用值传递还是指针传递呢?
2.性能分析
假设有一个 student struct,记录一个学生的基本信息。
type student struct {
id int
name string
sex byte
age int
addr string
}
假设有两个函数分别采用值传递和指针传递。
func PassByValue(s student) {
// Nothing to do.
}
func PassByPtr(s student) {
// Nothing to do.
}
然后写两个基准测试环境,放到 _test.go 结尾的测试文件中。
func BenchmarkPassByVal(b *testing.B) {
var s student
for i := 0; i < b.N; i++ {
PassByVal(s)
}
}
func BenchmarkPassByPtr(b *testing.B) {
var s student
for i := 0; i < b.N; i++ {
PassByPtr(&s)
}
}
假设将上面的代码放到 pass 目录下。下面我们执行基准测试命令,看下使用值传递和指针传递的性能差别。
go test -bench=^BenchmarkPass -benchmem -gcflags="-l" main/pass
goos: windows
goarch: amd64
pkg: main/pass
cpu: 11th Gen Intel(R) Core(TM) i5-11500 @ 2.70GHz
BenchmarkPassByVal-12 873122876 1.333 ns/op 0 B/op 0 allocs/op
BenchmarkPassByPtr-12 1000000000 0.6241 ns/op 0 B/op 0 allocs/op
PASS
ok main/pass 2.215s
-bench=^BenchmarkPass 指定要测试的基准函数名称以 BenchmarkPass 开头。-benchmem 打印基准测试的内存分配统计信息。-gcflags="-l" 表示禁止编译器的内联优化。
可见,当使用值传递 struct,因为发生拷贝的情况,所以性能比指针传递要低。
可以预见的是,随着 struct 大小增加,值传递和指针传递的性能差距会越来越大。
比如在上面的 student 中加入一个长度为 1024 字符的数组。
type student struct {
id int
name string
sex byte
age int
addr string
selfEvaluation [1024]rune
}
再次看下一下值传递和指针传递的性能差距。
go test -bench=^BenchmarkPass -benchmem -gcflags="-l" main/pass
goos: windows
goarch: amd64
pkg: main/pass
cpu: 11th Gen Intel(R) Core(TM) i5-11500 @ 2.70GHz
BenchmarkPassByVal-12 46635990 24.18 ns/op 0 B/op 0 allocs/op
BenchmarkPassByPtr-12 1000000000 0.6894 ns/op 0 B/op 0 allocs/op
PASS
ok main/pass 2.151s
可见,随着 struct 大小增加,值传递和指针传递的性能差距会越来越大。
3.结论
在 Golang 中,struct 可以使用值传递或指针传递进行传递。使用值传递时,函数会创建一个 struct 值的副本,并将该副本传递给函数。使用指针传递时,函数会传递 struct 值的指针。
使用指针传递 struct 通常比使用值传递更有效率,因为在值传递时需要将整个 struct 复制一份,并将其传递给函数。这可能会导致内存使用量增加,并且如果 struct 很大,可能会导致性能下降。
如果你需要在函数中修改 struct 的值,则必须使用指针传递。这是因为使用值传递传递 struct 副本时,对副本的任何更改都不会影响原始 struct。
如果你不需要在函数中修改 struct 的值,基于性能的考虑,建议使用指针传递。如果 struct 大小不大于指针的大小(如在 64 位系统上的 8 字节),那么就用值传递。
以上结论不仅仅适用于 struct,对于所有的大类型(如数组等),在选择值传递还是指针传递时,都应该考虑值拷贝带来的性能开销。