sync.Pool 异常行为解析:两类切片分配为何性能差异悬殊?
关于sync.Pool返回切片与切片指针的性能差异分析
测试场景与结果
为理解sync.Pool特性,我实现了两种New方法:
- 返回切片值
- 返回切片指针
同时搭配无池直接分配切片的方案做性能对比,基准测试结果如下: - 返回切片的池方案:6ns/op,1次内存分配
- 返回切片指针的池方案:17ns/op,0次内存分配
- 无池直接分配:130ns/op,1次内存分配
核心疑问
- 为何返回切片的池方案与无池直接分配方案均有内存分配,但性能差距巨大?
- 返回切片的池方案为何会产生内存分配?
代码复现
package main import ( "sync" ) func main() {} type MyBuffer struct { pool sync.Pool } func New() *MyBuffer { return &MyBuffer{pool: sync.Pool{ New: func() any { buf := make([]int, 0, 128) return &buf }, }} } func New2() *MyBuffer { return &MyBuffer{pool: sync.Pool{ New: func() any { buf := make([]int, 0, 128) return buf }, }} } func Fill[S ~[]E, E any](buf *S) { var some E for i := 0; i < cap(*buf); i++ { *buf = append(*buf, some) } } func BenchmarkAlloc(b *testing.B) { b.RunParallel(func(p *testing.PB) { for p.Next() { buf := make([]int, 0, 128) Fill(&buf) buf = buf[:0] } }) } func BenchmarkPoolPtr(b *testing.B) { mb := New() b.RunParallel(func(p *testing.PB) { for p.Next() { buf, _ := mb.pool.Get().(*[]int) Fill(buf) *buf = (*buf)[:0] mb.pool.Put(buf) } }) } func BenchmarkPoolNoPtr(b *testing.B) { mb := New2() b.RunParallel(func(p *testing.PB) { for p.Next() { buf, _ := mb.pool.Get().([]int) Fill(&buf) buf = buf[:0] mb.pool.Put(&buf) } }) }
问题解析
1. 返回切片的池方案为何有内存分配?
问题出在BenchmarkPoolNoPtr的Put(&buf)操作:
buf是循环内的局部切片变量,原本可分配在栈上;- 但你将
&buf(切片变量的地址)存入sync.Pool,而sync.Pool的存储结构在堆上,逃逸分析会判定buf必须分配到堆上(避免栈帧销毁后地址失效); - 因此每次循环都会在堆上分配一个24字节的切片结构(包含指针、长度、容量三个字段),这就是基准测试中1次内存分配的来源。
另外还存在类型不匹配的隐藏bug:
- New2的New函数返回切片值,但你Put的是切片指针;
- 后续Get操作时,类型断言
mb.pool.Get().([]int)会失败,返回零值切片; - 零值切片的cap为0,Fill函数内的循环不会执行,不会额外触发底层数组分配,但切片结构的堆分配已经存在。
2. 为何返回切片的池方案比无池直接分配快这么多?
两者的内存分配本质完全不同:
- 无池直接分配方案:每次
make([]int,0,128)会分配1024字节的底层数组(128个int,每个8字节),属于大内存块分配; - 返回切片的池方案:仅分配24字节的切片结构,底层数组从未被重新分配(第一次New时创建的底层数组在断言成功的情况下会被复用,断言失败时用的零值切片也不会触发底层数组分配);
- 小内存块的分配速度远快于大内存块,且sync.Pool在复用有效对象时也减少了部分分配开销,因此两者性能差距巨大。
3. 返回切片指针的池方案为何无内存分配?
- New函数返回的是切片指针,该指针指向的切片及其底层数组在第一次创建时就逃逸到堆上;
- 每次Get/Put操作复用的是同一个切片指针,切片结构和底层数组都不会被重新分配;
- Fill操作仅修改切片的长度,不涉及内存分配,因此整个过程无额外内存分配。
修正建议
如果想让返回切片的池方案真正复用底层数组,需修正BenchmarkPoolNoPtr的Put操作,改为Put切片值而非指针:
func BenchmarkPoolNoPtr(b *testing.B) { mb := New2() b.RunParallel(func(p *testing.PB) { for p.Next() { buf, _ := mb.pool.Get().([]int) Fill(&buf) buf = buf[:0] mb.pool.Put(buf) // 改为Put切片值,而非指针 } }) }
修正后,该方案的内存分配次数会降为0,性能会接近返回切片指针的池方案。
内容的提问来源于stack exchange,提问作者kostya
相关产品推荐
相关产品推荐

