sync.Pool 的文档只说“可能随时被移除”,但具体活多久?值不值得用?这篇实测对象能活过几次 GC(答案对应源码里的 victim cache),并量复用对分配、耗时和 GC 次数的影响,以及一个常见的装箱坑。
放入 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 的成本算进去(这里已经包含)。
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 的缓冲区就是例子)。本文没有测短寿命场景。