Go: bytebufferpool源码分析
Go: bytebufferpool源码分析
简介
bytebufferpool被广泛用于对于字节流的读取, 在gnet, fasthttp 等网络库中都有大量使用.
本文将探究其实现.
结构
项目主要分为两部分
-
bytebuffer
: 一个简单用来处理字节流的结构体, 提供了读/写/追加等能力, 实现比较简单 -
pool
:bytebuffer
的管理器, 实现稍微有些晦涩, 本文着重探究下其pool
的实现
代码实现
index
func index(n int) int {
n--
n >>= minBitSize
idx := 0
for n > 0 {
n >>= 1
idx++
}
if idx >= steps {
idx = steps - 1
}
return idx
}
inde是一个用于做分类的方法, 简单来说, 就是将一个长度映射到有限集合里
注意:
- 有限集的值域是[0, +...]
- $len(buffer) < (2^m), m = minBitSize$, 在这种情况下, 会直接置为0
- 其他情况直接对长度取余
Pool结构体
type Pool struct {
calls [steps]uint64 //某个长度被归档的次数, 用来统计长度的分布
calibrating uint64 // 调整计数器, 当数量超过calibrateCallsThreshold时, 重新调整
defaultSize uint64 // 调用Get时, 默认返回的长度
maxSize uint64 // 存入Pool的最大Size
pool sync.Pool // 实际的buffer缓存缓存池
}
主要方法
Get
func (p *Pool) Get() *ByteBuffer {
v := p.pool.Get()
if v != nil {
return v.(*ByteBuffer)
}
return &ByteBuffer{
B: make([]byte, 0, atomic.LoadUint64(&p.defaultSize)),
}
}
从Pool中获取一个长度缓存, 如果池子空了, 那么就使用defaultSize
初始化一个
Put
func (p *Pool) Put(b *ByteBuffer) {
idx := index(len(b.B))
if atomic.AddUint64(&p.calls[idx], 1) > calibrateCallsThreshold {
p.calibrate()
}
maxSize := int(atomic.LoadUint64(&p.maxSize))
if maxSize == 0 || cap(b.B) <= maxSize {
b.Reset()
p.pool.Put(b)
}
}
- 如果某个长度等级的使用超过阈值(
calibrateCallsThreshold (42000)
), 则进行调整 - 如果设置过
maxSize
且 是一个超长的buffer
, 则直接丢弃, 不存入pool中.
calibrate
代码比较长, 直接看注释吧
这段代码的思想非常有意思, 强烈建议阅读源代码, 仔细感受下.
func (p *Pool) calibrate() {
// 如果已经处于调整状态了, 则直接返回
if !atomic.CompareAndSwapUint64(&p.calibrating, 0, 1) {
return
}
a := make(callSizes, 0, steps)
var callsSum uint64
// 统计当前的长度等级的分布
for i := uint64(0); i < steps; i++ {
calls := atomic.SwapUint64(&p.calls[i], 0)
// 统计总和
callsSum += calls
a = append(a, callSize{
// 该长度等级调用次数
calls: calls,
// 长度
size: minSize << i,
})
}
// 对分布进行排序
sort.Sort(a)
// 出现次数最多的作为新的默认值
defaultSize := a[0].size
// 最大值一定 > 默认值
maxSize := defaultSize
// 只取 前95% 作为有效数据, 超出的部分作为边缘case忽略.
maxSum := uint64(float64(callsSum) * maxPercentile)
callsSum = 0
for i := 0; i < steps; i++ {
// 只统计到前95%
if callsSum > maxSum {
break
}
// 统计进度
callsSum += a[i].calls
// 更新最大size
size := a[i].size
if size > maxSize {
maxSize = size
}
}
// 更新默认值
atomic.StoreUint64(&p.defaultSize, defaultSize)
// 更新最大值
atomic.StoreUint64(&p.maxSize, maxSize)
// 更新调用状态
atomic.StoreUint64(&p.calibrating, 0)
}
注意
这段代码最需要咀嚼的是三个设计
- 动态的更新buffer的初始容量, 在不同的场景下都有着比较好的适应性
- 动态的更新最大值, 防止过大的buffer占用缓存空间, 导致内存占用 / GC异常
-
maxPercentile
的设计, 使得对以上两个值的设置具有代表性, 尤其是maxSize
不会因为偏差case, 导致异常的缓存.