You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

sync.Pool 异常行为解析:两类切片分配为何性能差异悬殊?

关于sync.Pool返回切片与切片指针的性能差异分析

测试场景与结果

为理解sync.Pool特性,我实现了两种New方法:

  • 返回切片值
  • 返回切片指针
    同时搭配无池直接分配切片的方案做性能对比,基准测试结果如下:
  • 返回切片的池方案:6ns/op,1次内存分配
  • 返回切片指针的池方案:17ns/op,0次内存分配
  • 无池直接分配:130ns/op,1次内存分配

核心疑问

  1. 为何返回切片的池方案与无池直接分配方案均有内存分配,但性能差距巨大?
  2. 返回切片的池方案为何会产生内存分配?

代码复现

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 01:20:32