关于 sync.Pool 的分析
1. sync.Pool 的使用
下面是常见的buffer池化代码
// sync.Pool 对象声明
var buffers = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
// 获取buffer对象
func GetBuffer() *bytes.Buffer {
return buffers.Get().(*bytes.Buffer)
}
// 清除记录, 将buffer对象还回池中
func PutBuffer(buf *bytes.Buffer) {
buf.Reset()
buffers.Put(buf)
}
benchmark
// 原生方式
func Benchmark_a(b *testing.B) {
for i := 0; i < b.N; i++ {
by := new(bytes.Buffer)
by.WriteString("xxxx1")
by.WriteString("xxxx2")
_ = by.String()
}
}
// buffer池方式
func Benchmark_b(b *testing.B) {
for i := 0; i < b.N; i++ {
by := GetBuffer()
by.WriteString("xxxx1")
by.WriteString("xxxx2")
_ = by.String()
PutBuffer(by)
}
}
Benchmark_a-12 29302688 38.70 ns/op 64 B/op 1 allocs/op
Benchmark_b-12 47637798 25.05 ns/op 0 B/op 0 allocs/op
通过 benchmark 结果可以看到, 使用buffer池后明显提升了性能.
第二列为b.N的值, 表示执行次数, 第三列 ns/op 表示平均每次操作花费的纳秒, 第四列 B/op 表示平均每次申请的内存大小, 最后一列 allocs/op 表示每次操作申请内存的次数.
2. sync.Pool 底层原理分析
type Pool struct {
noCopy noCopy
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
// It may not be changed concurrently with calls to Get.
New func() any
}
我们看下 Pool 的主要结构
字段名 | 含义 |
---|---|
local | local 是个数组,长度为 P 的个数。其元素类型是 poolLocal 。这里面存储着各个 P 对应的本地对象池。可以近似的看做 [P]poolLocal |
localSize | local 数组的长度(p的个数) |
victim | 与local结构一致, 用于下轮垃圾回收时清理的对象 |
localSize | local 数组的长度(p的个数) |
New | 对象生成函数(要池化的对象) |
通过以上我们发现, 池化后的数据其实核心数据结构在 local 对应的数组结构上
// Local per-P Pool appendix.
type poolLocalInternal struct {
private any // Can be used only by the respective P.
shared poolChain // Local P can pushHead/popHead; any P can popTail.
}
type poolLocal struct {
poolLocalInternal
// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
pool结构的local字段 指向的结构就是 poolLocal 数组, pad是为了内存对齐使用的, 我们先不用考虑. 主要结构是 poolLocalInternal, 里面的 private 字段是指向了一个new的对象, shared 指向了一个数组结构的链表.
通过上面的例子我们知道sync.Pool对象提供了几个方法, 我们看下他们的逻辑
- New 方法, 用于初始化一个对象
主要用于初始化原始对象, 虽然不是必须的, 但一般我们在代码初始化的时候使用, 当池中无对象或被垃圾回收时, 将会执行此方法.
- Get 方法, 从池中获取一个对象
用于获取一个对象, 获取流程如下图:
第一步先从当前p指向的 local 中获取, 获取 private 指向的对象(同时置为nil), 不存在则从本地对象池的链表中 pop head 获取, 再不存在从其他P对象池中 pop tail 对象.
第二步再从即将被垃圾清理掉的 victim 中获取对象, 获取private指向的对象, 不存在则从其他P中pop tail对象, 如果获取到了对象, 那么此对象不会在下次垃圾回收时清理掉.
获取对象流程中也有很多性能的考虑, 比如优先从当前P中获取对象, 避免了锁的争抢. 本地pop head、窃取其他P使用pop tail, 尽量降低出现争抢概率. lock-free优化机制, 使用atomic 包中的 CAS 操作
- Put方法比较简单, 使用完毕后调用, 用于将对象还到池中
第一步检查当前p指向的 local 下, private 指向的对象是否为nil, 如果为 nil 优先赋值 private.
第二步则将对象 push 到 shard 对象头部节点中.
3. sync.Pool 的问题
问题1: sync.Pool 有可能会导致内存泄漏, 比如文章最开头给出的例子, 如果buffer对象占用内存越来越大, 无法进行垃圾回收, 即使后续不会存储这么大容量的字符串, 内存也无法释放, 有导致内存泄漏的风险.
问题2: 池内的 buffer 占用内存都很大, 但还没到我们设定的最大阈值, 实际使用的时候,大部分只需要一个小的 buffer,导致内存空间的浪费
4. 如何优化 sync.Pool
针对问题1内存无法释放的问题, 我们可以在执行 put 回收对象时, 判断对象大小, 如果超过我们设定的阈值直接丢弃即可.
func PutBuffer(buf *bytes.Buffer) {
// 大于64kib, 则不还到池中
if buf.Cap() > 1>>16 {
return
}
buf.Reset()
buffers.Put(buf)
}
问题2可以参考 bytebufferpool 实现, 将根据占用占比自动调整占用内存区间, 检测最大的 buffer,超过最大尺寸的 buffer,就会被丢弃.
5. 参考
转载自:https://juejin.cn/post/7194082169982025787