bigywater.tech
Go · 74

sync.Pool:对象什么时候被回收,victim cache 是什么

sync.Pool 的文档只说“可能随时被移除”,但具体活多久?值不值得用?这篇实测对象能活过几次 GC(答案对应源码里的 victim cache),并量复用对分配、耗时和 GC 次数的影响,以及一个常见的装箱坑。

99 → 0
一次 GC 后还剩的对象数,两次 GC 后就是 0(放入 100 个)
714 → 192 ns
复用 bytes.Buffer 的单 goroutine 耗时,分配从 6 次降为 0
2611 → 0
200 万次借用 4 KiB 缓冲区的 GC 次数

victim cache 是什么

实测 · 本机执行输出
  放入 100 个对象,然后 runtime.GC() 0 次,再 Get 100 次:取到原来的 100 个,New 新造 0 个
  放入 100 个对象,然后 runtime.GC() 1 次,再 Get 100 次:取到原来的 99 个,New 新造 1 个
  放入 100 个对象,然后 runtime.GC() 2 次,再 Get 100 次:取到原来的 0 个,New 新造 100 个
  放入 100 个对象,然后 runtime.GC() 3 次,再 Get 100 次:取到原来的 0 个,New 新造 100 个

每次 GC 开头,poolCleanup 先丢掉上一轮的 victim 缓存,再把当前的主缓存挪成 victim 缓存。对象因此要经过两次 GC 才会消失:实验里放入 100 个,一次 GC 后还能取回 99 个,两次后一个不剩。(一次 GC 少的那个,我没有查。)

放指针,别放值

实测 · 本机执行输出
  Put([]byte)(把切片本身放进去):每次 1 次分配
  Put(&s)(放指针):每次 0 次分配

往池里 Put 一个切片会因为装箱多一次分配,放它的指针就没有。这是用 Pool 时最常见的无意义开销。

复用的收益

实测 · 本机执行输出
  每次新建 bytes.Buffer(单 goroutine)        714 ns/次    4032 B/次  6 次分配/次
  sync.Pool 复用(单 goroutine)             192 ns/次       0 B/次  0 次分配/次
  每次新建(4 个 P 并行)                        987 ns/次    4032 B/次  6 次分配/次
  sync.Pool 复用(4 个 P 并行)                 71 ns/次       0 B/次  0 次分配/次

每次写约 2 KB 的 bytes.Buffer,新建每次 714 ns、6 次分配、4032 B;用池复用后 192 ns、0 次分配。4 个 P 并行时 987 ns 对 71 ns。做这种测试时要把 Reset 的成本算进去(这里已经包含)。

对 GC 的影响

实测 · 本机执行输出
  200 万次借用 4 KiB 缓冲区,每次 make:GC 2611 次,累计分配 8192 MB
  200 万次借用 4 KiB 缓冲区,用 sync.Pool:GC 0 次,累计分配 0 MB

200 万次借用 4 KiB 缓冲区,每次 make 触发了 2611 次 GC、累计分配 8192 MB,用池复用时一次 GC 都没触发。这是一个理想化的循环,真实服务里池里的对象仍然会因为 GC 而丢失重建。

什么时候不该用

实测 · 本机执行输出
  
      // Drop victim caches from all pools.
      for _, p := range oldPools {
          p.victim = nil
          p.victimSize = 0
      }
  
      // Move primary cache to victim cache.
      for _, p := range allPools {
          p.victim = p.local
          p.victimSize = p.localSize
          p.local = nil
          p.localSize = 0
      }
  
      // The pools with non-empty primary caches now have non-empty
  文档:A Pool is a set of temporary objects that may be individually saved and
  文档:Any item stored in the Pool may be removed automatically at any time without

文档的建议:短寿命对象自己持有的空闲链表不适合用 Pool;Pool 适合在并发的独立使用者之间共享临时对象(fmt 的缓冲区就是例子)。本文没有测短寿命场景。

分步演示

sync.Pool
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。生存实验每组新建一个 Pool,GOMAXPROCS=4;基准用 testing.Benchmark 与 RunParallel;GC 次数来自 runtime.MemStats;读过的源码与文档:sync/pool.go 的 poolCleanup、go doc sync.Pool
    • 没有验证:一次 GC 后少的 1 个对象去了哪、Pool 的 per-P 结构(private/shared 链)细节、对象特别大或特别多时的表现、Pool 与 finalizer 的交互、短寿命场景下的收益、其它架构
    京ICP备2026067502号-1京公网安备11010502063487号