关于 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对象提供了几个方法, 我们看下他们的逻辑

  1. New 方法, 用于初始化一个对象

主要用于初始化原始对象, 虽然不是必须的, 但一般我们在代码初始化的时候使用, 当池中无对象或被垃圾回收时, 将会执行此方法.

  1. Get 方法, 从池中获取一个对象

用于获取一个对象, 获取流程如下图:

image.png

扫描二维码关注公众号,回复: 14565751 查看本文章

第一步先从当前p指向的 local 中获取, 获取 private 指向的对象(同时置为nil), 不存在则从本地对象池的链表中 pop head 获取, 再不存在从其他P对象池中 pop tail 对象.

第二步再从即将被垃圾清理掉的 victim 中获取对象, 获取private指向的对象, 不存在则从其他P中pop tail对象, 如果获取到了对象, 那么此对象不会在下次垃圾回收时清理掉.

获取对象流程中也有很多性能的考虑, 比如优先从当前P中获取对象, 避免了锁的争抢. 本地pop head、窃取其他P使用pop tail, 尽量降低出现争抢概率. lock-free优化机制, 使用atomic 包中的 CAS 操作

  1. 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. 参考

time.geekbang.org/column/arti…

www.cyhone.com/articles/th…

github.com/valyala/byt…

blog.haohtml.com/archives/30…

猜你喜欢

转载自juejin.im/post/7194082169982025787
今日推荐