Go: bytebufferpool源码分析

Go: bytebufferpool源码分析

项目地址: https://github.com/valyala/bytebufferpool

简介

bytebufferpool被广泛用于对于字节流的读取, 在gnet, fasthttp 等网络库中都有大量使用.

本文将探究其实现.

结构

项目主要分为两部分

  1. bytebuffer​: 一个简单用来处理字节流的结构体, 提供了读/写/追加等能力, 实现比较简单
  2. 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是一个用于做分类的方法, 简单来说, 就是将一个长度映射到有限集合里

注意:

  1. 有限集的值域是[0, +...]
  2. $len(buffer) < (2^m), m = minBitSize$, 在这种情况下, 会直接置为0
  3. 其他情况直接对长度取余

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)
	}
}
  1. 如果某个长度等级的使用超过阈值(calibrateCallsThreshold (42000)​), 则进行调整
  2. 如果设置过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)
}

注意

这段代码最需要咀嚼的是三个设计

  1. 动态的更新buffer的初始容量, 在不同的场景下都有着比较好的适应性
  2. 动态的更新最大值, 防止过大的buffer占用缓存空间, 导致内存占用 / GC异常
  3. maxPercentile​的设计, 使得对以上两个值的设置具有代表性, 尤其是maxSize​不会因为偏差case, 导致异常的缓存.

引申阅读

go-buffer-pool

posted @ 2024-03-26 16:26  pDJJq  阅读(15)  评论(0编辑  收藏  举报