Go语言指针切片为何产生大量内存分配?
为什么Go中结构体指针切片的内存分配远多于结构体切片?
核心原因:指针切片的每个元素都对应一次堆内存分配
你的基准测试结果差异,根本原因不是append操作,而是结构体指针的内存分配方式和Go的逃逸分析机制:
1. 指针切片的高分配次数来源
在BenchmarkSmallStructPointer中:
- 每次调用
newSmallStruct都会创建一个smallStruct实例并返回其指针。由于这个指针被传递到函数外部(benchmark循环的切片中),Go的逃逸分析会判定该结构体无法在栈上分配,必须放到堆中。 - 循环执行10000次,就会产生10000次独立的堆内存分配,再加上切片本身的1次底层数组分配,总共10001次分配——这和基准结果里的
10001 allocs/op完全对应。 - 64位系统中,每个指针占8字节,10000个指针就是80000字节;每个
smallStruct占8字节(int类型),10000个实例又是80000字节,加起来约160000字节,和161921 B/op的内存消耗一致。
2. 结构体切片的优化逻辑
在BenchmarkSmallStruct中:
- 虽然代码调用了
newSmallStruct,但后续的*t是将指针指向的结构体值拷贝到切片的底层数组中。 - Go编译器的优化会发现,这里不需要将结构体分配到堆上:可以直接在栈上构造
smallStruct实例,然后拷贝到预分配好的切片底层数组里,完全避免了堆分配。 - 切片本身通过
make(..., 0, 10000)预分配了足够的容量,只需要1次分配来创建底层数组——对应基准结果里的1 allocs/op。 - 10000个
smallStruct实例直接存储在切片底层数组中,每个占8字节,总共80000字节,和81920 B/op的内存消耗一致(少量额外字节是切片元数据)。
3. 误区澄清:append不是罪魁祸首
你提到怀疑是append导致的,但实际上两次测试中的append都不会触发额外分配——因为你已经通过make指定了足够的容量(10000),append只是将元素写入已有的底层数组,不会触发扩容。指针切片的高分配次数完全来自于每个结构体实例的堆分配。
测试代码
package pointer import ( "testing" ) type smallStruct struct { ID int } func newSmallStruct(id int) *smallStruct { return &smallStruct{ID: id} } func BenchmarkSmallStructPointer(b *testing.B) { for n := 0; n < b.N; n++ { var slice = make([]*smallStruct, 0, 10000) for i := 0; i < 10000; i++ { t := newSmallStruct(n + i) slice = append(slice, t) } } } func BenchmarkSmallStruct(b *testing.B) { for n := 0; n < b.N; n++ { var slice = make([]smallStruct, 0, 10000) for i := 0; i < 10000; i++ { t := newSmallStruct(n + i) slice = append(slice, *t) } } }
基准测试结果
go test -bench . -benchmem goos: linux goarch: amd64 pkg: test-project/pointer cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz BenchmarkSmallStructPointer-4 3121 328864 ns/op 161921 B/op 10001 allocs/op BenchmarkSmallStruct-4 29218 48021 ns/op 81920 B/op 1 allocs/op
内容的提问来源于stack exchange,提问作者Dmitry Ermichev
相关产品推荐
相关产品推荐

