GWS 项目的秘密武器:高性能 bytes.Buffer 池设计详解

首页 编程分享 PHP丨JAVA丨OTHER 正文

路口IT大叔_KUMA 转载 编程分享 2024-09-14 22:07:17

简介 本文分享了作者在 IT 行业的经验,详细介绍了 Go 语言中 bytes.Buffer 的工作原理及其在 GWS 项目中的高效内存池设计,提供了深入的技术见解和实践心得。


我是 LEE,老李,一个在 IT 行业摸爬滚打 17 年的技术老兵。

又是非常长的一段时间没有更新了,最近一直在忙于工作和生活,没有太多的时间来写文章。但是我并没有放弃我热爱的技术,也没有放弃我热爱的写作。实际上,在这段时间里,我一直在向 lxzan 兄探讨关于 GWS 的一些问题,尤其是内部具体实现的原因以及设计想法。今天,我想和大家分享一下我最近的一些思考和感悟。

对于 GWS 这个项目的了解是一个巧合。当时 gorilla/websocket 这个项目的作者无法继续维护,准备将项目归档。为了保持代码的可维护性,我决定寻找一个替代方案。果不其然,我发现了 GWS。当时我就在想,这个项目是否可以替代 gorilla/websocket 呢?不巧的是,看到作者介绍自己的项目时,就提到让 gorilla/websocket 的使用者能够以最少的代码迁移到 GWS。突然,我整个人都兴奋起来了,感觉这个项目就是我想要的。通过一段时间的简单阅读,我发现 GWS 的代码复杂度远低于 gorilla/websocket,而且性能也更好。所以我决定尝试一下。

经过一段时间的尝试,我在 2023 年的时候,在公司项目中将 gorilla/websocket 替换成了 GWS。通过当年各种实际生产的验证,发现 GWS 确实可以替代 gorilla/websocket,而且性能确实比 gorilla/websocket 要好很多。最后,我决定投身于 GWS 的研究中,并与 lxzan 兄建立了联系。

这里有一个非常小的插曲。当时说要给 lxzan 兄的项目贡献代码和写文章,但没想到这一鸽就是一年,说来惭愧。现在 GWS 库的代码基本稳定,迭代的部分已经非常少。所以前一段时间,我冒了天下之大不韪,给 GWS 代码做了批注。这一批注不要紧,却让我重新对 GWS 有了更深的理解。这不仅让我感觉到自己还有很多地方可以学习,也让我思考了代码编写时的一些方式、解决问题的方式,以及如何在功能性和性能之间做出取舍。

好了,废话不多说,让我们开始吧。

1. 一切从这开始:bytes.Buffer

有意思,为什么要用一个 Go 库作为文章的第一个标题?因为 GWS 的代码中大量使用了 bytes.Buffer,这些 bytes.Buffer 是通过池的方式提供的。当然,在介绍 GWS 中的 bytes.Buffer 池之前,需要对 Gobytes.Buffer 源代码进行拆解,以及 sync.Pool 实现 bytes.Buffer 池的原因。为什么不能与 GWS 中的 bytes.Buffer 池对比,以及 lxzan 兄弟说他对 GWS 中的 bytes.Buffer 池编码了几十遍才达到现在的效果。

1.1 bytes.Buffer 是什么?

bytes.BufferGo 标准库中的 []byte 缓冲区(流式缓冲区),具有读写方法和可变大小的字节存储功能。缓冲区的零值是一个待使用的空缓冲区。可以持续向 Buffer 尾部写入数据,从 Buffer 头部读取数据。当 Buffer 内部空间不足以满足写入数据的大小时,会自动扩容。

bytes.Buffer 结构定义如下:

// A Buffer is a variable-sized buffer of bytes with Read and Write methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
	buf      []byte // contents are the bytes buf[off : len(buf)]
	off      int    // read at &buf[off], write at &buf[len(buf)]
	lastRead readOp // last read operation, so that Unread* can work correctly.
}
  • buf:底层的缓冲字节切片,用于保存数据。len(buf) 表示字节切片长度,cap(buf) 表示切片容量。
  • off:已读计数,在该位置之前的数据都是被读取过的,off 表示下次读取时的开始位置。因此未读数据部分为 buf[off:len(buf)]
  • lastRead:保存上次的读操作类型,用于后续的回退操作。

有了上面的直观感受后,我们继续深入 bytes.Buffer 的内部结构。从某种程度上,我们可以将 bytes.Buffer 理解为对 []byte 的包装。Go 官方提供了很多方法来操作 []byte,比如 WriteReadReadFromWriteTo 等等。

回到重点,[]byte 是一个动态数组,它的长度和容量是可变的。也就是说,它是一个 slice,底层映射一个数组。

我们都知道在 Go 的世界中如何使用代码定义数组和切片,如下是具体代码:

var a1 = [10]int{1, 2, 3} // 数组,元素长度是固定的
var a2 = []int{1, 2, 3} // 切片,元素长度是可变的

如果在 a1 中追加代码,超过 10 个元素时会报错。然而,a2 不会,因为 a2 是一个切片,其长度和容量是可变的。当 a2 需要更多空间时,它会重新分配内存并将原数据拷贝到新内存中。这就是切片的扩容机制。当然,扩容的长度和容量是根据 Go 的扩容算法决定的,本文章并不打算深入探讨 Go 的扩容算法。

1.2 bytes.Buffer 的扩容机制

让我们把目光转向 bytes.Bufferbuf 是一个 []byte,其长度和容量是可变的。也就是说,一旦使用 Write 方法向 bytes.Buffer 写入数据,如果 bytes.Buffer 的容量不够,它会自动扩容。具体来说,buf 会重新分配内存并将原数据拷贝到新内存中。这个问题在日常使用中影响不大,但在高性能和高并发的系统中,频繁的扩容会导致性能下降。

接下来,我们来看一下 bytes.Buffer 的源代码。

Write 方法

// Write appends the contents of p to the buffer, growing the buffer as
// needed. The return value n is the length of p; err is always nil. If the
// buffer becomes too large, Write will panic with ErrTooLarge.
func (b *Buffer) Write(p []byte) (n int, err error) {
	b.lastRead = opInvalid
	m, ok := b.tryGrowByReslice(len(p))
	if !ok {
		m = b.grow(len(p))
	}
	return copy(b.buf[m:], p), nil
}
  • tryGrowByReslice 方法尝试判断是否需要扩容。如果需要扩容,则执行 grow 方法,扩容长度为 len(p) 的空间块,并返回扩容后 buf 的索引位置。如果不需要扩容,则直接返回 buf 的索引位置。
  • copy 方法将 p 中的内容复制到 b.buf[m:] 后面的空间中。
  • grow 方法用于扩容 buf,并返回扩容后 buf 的索引位置。

tryGrowByReslice 方法

tryGrowByReslice 方法尝试通过重新切片来扩容 buf。如果 buf 的容量足够,则返回 buf 的索引位置,否则返回 0falselbuf 的长度,cap(b.buf)-lbuf 的剩余容量。如果 n 小于等于 buf 的剩余容量,则返回 ltrue,否则返回 0false

// tryGrowByReslice is an inlineable version of grow for the fast-case where the
// internal buffer only needs to be resliced.
// It returns the index where bytes should be written and whether it succeeded.
func (b *Buffer) tryGrowByReslice(n int) (int, bool) {
	if l := len(b.buf); n <= cap(b.buf)-l {
		b.buf = b.buf[:l+n]
		return l, true
	}
	return 0, false
}

grow 方法

grow 方法用于扩展 Buffer 的内部缓冲区,以确保有足够的空间来存储额外的 n 个字节。

// grow grows the buffer to guarantee space for n more bytes.
// It returns the index where bytes should be written.
// If the buffer can't grow it will panic with ErrTooLarge.
func (b *Buffer) grow(n int) int {
	m := b.Len()
	// If buffer is empty, reset to recover space.
	if m == 0 && b.off != 0 {
		b.Reset()
	}
	// Try to grow by means of a reslice.
	if i, ok := b.tryGrowByReslice(n); ok {
		return i
	}
	if b.buf == nil && n <= smallBufferSize {
		b.buf = make([]byte, n, smallBufferSize)
		return 0
	}
	c := cap(b.buf)
	if n <= c/2-m {
		// We can slide things down instead of allocating a new
		// slice. We only need m+n <= c to slide, but
		// we instead let capacity get twice as large so we
		// don't spend all our time copying.
		copy(b.buf, b.buf[b.off:])
	} else if c > maxInt-c-n {
		panic(ErrTooLarge)
	} else {
		// Add b.off to account for b.buf[:b.off] being sliced off the front.
		b.buf = growSlice(b.buf[b.off:], b.off+n)
	}
	// Restore b.off and len(b.buf).
	b.off = 0
	b.buf = b.buf[:m+n]
	return m
}
  1. 初始检查和重置:如果缓冲区为空 (b.Len() == 0) 且偏移量 b.off 不为零,则调用 b.Reset() 重置缓冲区以回收空间。
  2. 尝试通过重新切片来增长:调用 b.tryGrowByReslice(n) 尝试通过重新切片来增长缓冲区。如果成功,返回新的写入位置。
  3. 初始化缓冲区:如果缓冲区为空且 n 小于等于 smallBufferSize,则创建一个新的缓冲区并返回索引 0。
  4. 滑动现有数据:如果缓冲区的容量 c 足够大,可以通过滑动现有数据来腾出空间。具体来说,如果 n 小于等于 c/2 - m,则将数据复制到缓冲区的起始位置。
  5. 处理容量不足的情况:如果容量 c 不足以容纳 n 个字节,且 c + n 超过了 maxInt,则抛出 ErrTooLarge 错误。否则,调用 growSlice 函数来扩展缓冲区,并更新缓冲区的偏移量和长度。
  6. 返回写入位置:返回新的写入位置 m,即缓冲区的当前长度。

到这里我基本算讲清楚了 bytes.Buffer 的内部结构和扩容机制。

1.3 bytes.Buffer 扩容的影响

bytes.Buffer 的扩容机制对于高性能系统的影响主要体现在以下几个方面:

  1. 内存分配和释放

    • bytes.Buffer 的扩容机制涉及动态内存分配。频繁的内存分配和释放会导致垃圾回收(GC)的压力增大,尤其是在高并发场景下,可能会导致系统性能下降。
    • 为了避免频繁的内存分配,bytes.Buffer 在扩容时会预留一定的空间,减少后续扩容的次数。
  2. 数据复制

    • bytes.Buffer 的内部缓冲区不足以容纳新数据时,需要将现有数据复制到新的更大的缓冲区中。数据复制操作会消耗 CPU 资源,尤其是在缓冲区数据量较大时,可能会成为性能瓶颈。
    • bytes.Buffer 通过预先分配更大的缓冲区来减少数据复制的次数,从而提高性能。
  3. 并发访问

    • 在高并发系统中,多个 goroutine 可能同时访问同一个 bytes.Buffer。如果没有适当的同步机制,可能会导致数据竞争和内存泄漏。
    • 使用 sync.Pool 可以有效地管理 bytes.Buffer 实例,减少内存分配和垃圾回收的开销,提高系统的并发性能。
  4. 预分配策略

    • bytes.Buffer 提供了 Grow 方法,允许开发者预先分配足够的空间,避免在写入数据时频繁扩容。预分配策略可以显著减少内存分配和数据复制的次数,提高系统的整体性能。
  5. 垃圾回收

    • 频繁的内存分配和释放会导致垃圾回收器(GC)频繁运行,增加系统的延迟。通过合理使用 bytes.Buffer 的扩容机制,可以减少不必要的内存分配,降低 GC 的压力,从而提高系统的响应速度。

2. GWS 如何设计 bytes.Buffer

在具体介绍 GWS 项目中的实现之前,我们先看看传统编写代码的方式。写得有点乱和繁杂,请看官们多多包涵。

2.1 基于 sync.Pool 实现的 bytes.Buffer

我们在编写传统的 sync.Pool 实现 bytes.Buffer 池时,通常沿用一些模板代码,而且我在很多 GitHub 的项目中,包括自己写的很多代码中都是类似的。具体代码如下:

通用 bytes.Buffer

var bufferPool = sync.Pool{
    New: func() interface{} {
        return &bytes.Buffer{make([]byte, 0, 1024)}
    },
}

创建一个 BufferPool,用于存储 bytes.Buffer 对象。每个 bytes.Buffer 对象的初始容量为 1024 字节。

接下来,我们可以使用 bufferPoolGetPut 对象了。

func GetBuffer() *bytes.Buffer {
	return bufferPool.Get().(*bytes.Buffer)
}

func PutBuffer(buf *bytes.Buffer) {
	buf.Reset()
	bufferPool.Put(buf)
}

写到这里,一个 BufferPool 就已经实现完毕。得益于 Gosync.Pool,我们轻松拥有了高效能的 bytes.Buffer 池。在一般场景下,这样的实现可能没有太多问题,但在高性能和高并发的系统中,频繁的扩容会导致性能下降。

你可能觉得我在危言耸听,先来看看这张图,给你一个直观的感受。

我们通过随机数算法生成长度在 0-65535 之间的字符串,反复调用 GetBufferPutBuffer 方法,同时将生成的随机字符串写入 bytes.Buffer 中,并记录写入的 Len()Cap() 的返回值,然后进行绘图。

从图的左侧可以看到,生成的字符串长度基本上在每个长度上相差不大,体现了随机数的随机性和离散性。我们大致可以认为这个随机数是一个“白噪声”随机数。

但图的右边却很有意思:在 0-30000 范围内,容量分布较为密集,每个 Capacity 都有一定的调用次数。而在 30000-65535 范围内,容量分布稀疏,每个 Capacity 的调用次数非常高。这证明了 GetBuffer 方法并不能每次都使用最合理容量的 bytes.Buffer 对象。

回到我们之前讨论的 slice 的扩容机制,一个初始容量为 1024 的切片要装下长度 60000 的字符串,可能需要多次扩容和内存复制。

这里我介绍一下:一个 cap1024 的切片要装下一个长度 60000 的字符串,是如何扩容的

初始容量为 1024 字节的切片,在写入 60000 字节的数据时,需要经过多次扩容,最终容量达到 65536 字节。每次扩容都会将容量翻倍,直到满足需求。这种扩容策略虽然简单,但在数据量较大时会导致频繁的内存分配和数据复制,影响性能。

详细的扩容过程如下:

  1. 初始状态

    • 切片 s 的长度 (len) = 0
    • 切片 s 的容量 (cap) = 1024
    • 底层数组大小 = 1024 字节
  2. 第一次扩容(1024 -> 1280)

    • 需要 60000 个字节,超过当前容量 1024
    • 新容量 = 1024 * 1.25 = 1280
    • 分配新的 1280 字节数组
    • 复制原有数据(此例中为空)到新数组
    • 更新切片结构指向新数组
    • 旧的 1024 字节数组被标记为可回收
  3. 第二次扩容(1280 -> 1600)

    • 1280 仍不足,继续扩容
    • 新容量 = 1280 * 1.25 = 1600
    • 分配新的 1600 字节数组
    • 复制当前数据到新数组
    • 更新切片结构
    • 旧的 1280 字节数组被标记为可回收
  4. 持续扩容

    • 这个过程继续,每次增加约 1.25 倍
    • 2000 -> 2500 -> 3125 -> 3906 -> 4882 -> ...
    • 每次都会创建新数组,复制数据,更新切片结构
  5. 最后一次扩容(约 48828 -> 61035)

    • 当新容量首次超过 60000 时停止
    • 新容量 = 48828 * 1.25 ≈ 61035
    • 分配新的 61035 字节数组
    • 复制当前数据到新数组
    • 更新切片结构
    • 旧数组被标记为可回收
  6. 添加新数据

    • 60000 个字节被复制到新数组中,从索引 0 开始
    • 使用优化的内存复制函数(如 memmove
  7. 最终状态

    • 切片长度 (len) = 60000
    • 切片容量 (cap) = 61035
    • 底层数组大小 = 61035 字节
  8. 内存管理

    • 所有中间产生的数组(1024, 1280, 1600, ...)都被标记为可回收
    • 这些数组将在下一次垃圾回收时被释放

2.2 GWSbytes.Buffer 池设计

终于到了本文的高潮部分,铺垫了这么多就是为了引出 GWSbytes.Buffer 池设计。回到文章开头的故事,在没有注释整个 GWS 代码之前,我并没有意识到 GWSbytes.Buffer 池设计是如此的精妙。

之前 lxzan 兄还耐心地给我讲解了 GWSbytes.Buffer 池设计,当时我并没有完全理解。甚至一直以为不过如此,就是简单的处理下,直到我想对 GWSbytes.Buffer 池性能做一个统计分析的时候,才发现自己大错特错。

我们一起来欣赏下 GWSbytes.Buffer 池的代码:

type BufferPool struct {
	begin  int
	end    int
	shards map[int]*sync.Pool
}

// NewBufferPool 创建一个内存池
// creates a memory pool
// left 和 right 表示内存池的区间范围,它们将被转换为 2 的 n 次幂
// left and right indicate the interval range of the memory pool, they will be transformed into pow(2, n)
// 小于 left 的情况下,Get 方法将返回至少 left 字节的缓冲区;大于 right 的情况下,Put 方法不会回收缓冲区
// Below left, the Get method will return at least left bytes; above right, the Put method will not reclaim the buffer
func NewBufferPool(left, right uint32) *BufferPool {
	var begin, end = int(binaryCeil(left)), int(binaryCeil(right))
	var p = &BufferPool{
		begin:  begin,
		end:    end,
		shards: map[int]*sync.Pool{},
	}
	for i := begin; i <= end; i *= 2 {
		capacity := i
		p.shards[i] = &sync.Pool{
			New: func() any { return bytes.NewBuffer(make([]byte, 0, capacity)) },
		}
	}
	return p
}

// Put 将缓冲区放回到内存池
// returns the buffer to the memory pool
func (p *BufferPool) Put(b *bytes.Buffer) {
	if b != nil {
		if pool, ok := p.shards[b.Cap()]; ok {
			pool.Put(b)
		}
	}
}

// Get 从内存池中获取一个至少 n 字节的缓冲区
// fetches a buffer from the memory pool, of at least n bytes
func (p *BufferPool) Get(n int) *bytes.Buffer {
	var size = Max(int(binaryCeil(uint32(n))), p.begin)
	if pool, ok := p.shards[size]; ok {
		b := pool.Get().(*bytes.Buffer)
		if b.Cap() < size {
			b.Grow(size)
		}
		b.Reset()
		return b
	}
	return bytes.NewBuffer(make([]byte, 0, n))
}

// binaryCeil 将给定的 uint32 值向上取整到最近的 2 的幂
// rounds up the given uint32 value to the nearest power of 2
func binaryCeil(v uint32) uint32 {
	v--
	v |= v >> 1
	v |= v >> 2
	v |= v >> 4
	v |= v >> 8
	v |= v >> 16
	v++
	return v
}

是不是很简单?正因为这段代码简单到只有几十行,曾经让我一度以为不过如此。真实情况是,这个代码的实现非常精妙,而且性能非常好。

这段代码实现了一个高效的 bytes.Buffer 池,其设计非常巧妙。我将从几个关键点来解释这个实现:

  1. 分片设计

    BufferPool 结构使用了分片设计,通过 shards 字段存储不同容量的 sync.Pool。这种设计允许池根据不同的容量需求提供相应大小的缓冲区,减少内存浪费。

  2. 容量范围

    beginend 字段定义了池管理的缓冲区容量范围。这个范围被转换为 2 的幂次,确保所有管理的缓冲区大小都是 2 的幂次,有利于内存对齐和管理。

  3. 初始化

    NewBufferPool 函数初始化池,为每个 2 的幂次容量创建一个 sync.Pool。这确保了池可以提供一系列预定义大小的缓冲区。

  4. 二进制上限函数

    binaryCeil 函数是一个巧妙的位操作实现,用于将给定值向上取整到最近的 2 的幂。这个函数保证了所有管理的缓冲区大小都是 2 的幂次。

  5. 获取缓冲区

    Get 方法首先计算所需的缓冲区大小(向上取整到 2 的幂次),然后从相应的 sync.Pool 中获取缓冲区。如果获取的缓冲区容量不足,会进行扩容。

  6. 归还缓冲区

    Put 方法根据缓冲区的容量,将其归还到相应的 sync.Pool 中。这确保了缓冲区被重用时,其容量是合适的。

  7. 容量不足时的处理

    如果请求的容量超出了池管理的最大容量,Get 方法会创建一个新的缓冲区而不是从池中获取。这避免了池管理过大的缓冲区。

这个设计的特别之处在于:

  1. 精确的容量管理:通过 2 的幂次划分,减少了内存碎片和浪费。
  2. 高效的内存分配:预先分配不同大小的缓冲区,减少运行时的内存分配。
  3. 灵活的扩展性:可以根据需求调整容量范围。
  4. 优化的性能:使用位操作和 2 的幂次,提高了计算效率。

就这么说吧:这个实现在内存效率和性能之间取得了很好的平衡,特别适合高并发和高性能的场景。

为了更好地理解 GWSbytes.Buffer 的效能,我这里也上一张图,方便大家能够有一个直观的感受。

我们通过随机数算法生成长度在 0-65535 之间的字符串,反复调用 GetPut 方法,同时将生成的随机字符串写入 bytes.Buffer 中,并记录写入的 Len()Cap() 的返回值,然后进行绘图。

从图的左侧可以看到,生成的字符串长度基本上在每个长度上相差不大,体现了随机数的随机性和离散性。我们大致可以认为这个随机数是一个“白噪声”随机数。

以上的整个测试前置条件都跟 sync.Pool 的测试一致。

但图的右边却很有意思:容量排列非常稀疏,数值呈现了指数分布,而且每个 Capacity 的调用次数非常高。这证明了 Get 方法每次都使用最合理容量的 bytes.Buffer 对象。

2.2.1 NewBufferPool 函数

NewBufferPool 函数是 BufferPool 的构造函数:

func NewBufferPool(left, right uint32) *BufferPool {
    var begin, end = int(binaryCeil(left)), int(binaryCeil(right))
    var p = &BufferPool{
        begin:  begin,
        end:    end,
        shards: map[int]*sync.Pool{},
    }
    for i := begin; i <= end; i *= 2 {
        capacity := i
        p.shards[i] = &sync.Pool{
            New: func() any { return bytes.NewBuffer(make([]byte, 0, capacity)) },
        }
    }
    return p
}

这个函数的特点包括:

  1. 容量范围的二进制上限:使用 binaryCeil 函数将输入的 leftright 转换为最接近的 2 的幂次。这确保了所有管理的缓冲区大小都是 2 的幂次,有利于内存对齐和管理。
  2. 分片池初始化:在给定的容量范围内,为每个 2 的幂次容量创建一个 sync.Pool。这种设计允许池根据不同的容量需求提供相应大小的缓冲区,减少内存浪费。
  3. 闭包使用:在创建每个 sync.Pool 时,使用闭包来捕获当前的 capacity 值。这确保了每个池都能创建正确容量的缓冲区。

2.2.2 Get 方法

Get 方法用于从内存池中获取一个至少 n 字节的缓冲区:

func (p *BufferPool) Get(n int) *bytes.Buffer {
    var size = Max(int(binaryCeil(uint32(n))), p.begin)
    if pool, ok := p.shards[size]; ok {
        b := pool.Get().(*bytes.Buffer)
        if b.Cap() < size {
            b.Grow(size)
        }
        b.Reset()
        return b
    }
    return bytes.NewBuffer(make([]byte, 0, n))
}

这个方法的设计考虑了以下几点:

  1. 容量向上取整:使用 binaryCeil 函数将请求的容量向上取整到最近的 2 的幂次。这确保了获取的缓冲区容量总是足够的,同时也符合池的分片设计。
  2. 最小容量保证:通过 Max(int(binaryCeil(uint32(n))), p.begin) 确保返回的缓冲区至少具有 p.begin 指定的容量。
  3. 缓冲区复用:首先尝试从对应容量的 sync.Pool 中获取缓冲区。如果成功,会检查并确保缓冲区容量足够,然后重置缓冲区以供使用。
  4. 容量不足时的处理:如果请求的容量超出了池管理的最大容量,会创建一个新的缓冲区而不是从池中获取。这避免了池管理过大的缓冲区。

2.2.3 Put 方法

Put 方法用于将缓冲区放回到内存池:

func (p *BufferPool) Put(b *bytes.Buffer) {
    if b != nil {
        if pool, ok := p.shards[b.Cap()]; ok {
            pool.Put(b)
        }
    }
}

这个方法的设计考虑了以下几点:

  1. 空值检查:首先检查传入的缓冲区是否为 nil,避免空指针异常。
  2. 精确匹配:根据缓冲区的实际容量 (b.Cap()) 找到对应的 sync.Pool。这确保了缓冲区被放回到正确容量的池中。
  3. 容量超限处理:如果缓冲区的容量超出了池管理的范围(即找不到对应的 sync.Pool),该缓冲区不会被放回池中。这避免了池管理过大的缓冲区,有助于控制内存使用。
  4. 简单高效:方法实现简单直接,没有多余的操作,保证了高效的性能。

3. 总结

GWSbytes.Buffer 池设计展现了高效能和精巧的工程思维。这个设计在简洁性和性能之间取得了令人印象深刻的平衡,特别适合高并发和高性能的场景。让我们回顾一下这个设计的关键特点:

  1. 分片池设计:通过 shards 字段存储不同容量的 sync.Pool,允许池根据不同的容量需求提供相应大小的缓冲区,有效减少了内存浪费。
  2. 2 的幂次容量管理:所有管理的缓冲区大小都是 2 的幂次,这不仅有利于内存对齐和管理,还提高了计算效率。
  3. 灵活的容量范围:通过 beginend 字段定义池管理的缓冲区容量范围,可以根据实际需求进行调整。
  4. 高效的二进制上限函数binaryCeil 函数使用巧妙的位操作,快速将给定值向上取整到最近的 2 的幂。
  5. 智能的缓冲区获取Get 方法会计算所需的缓冲区大小并从相应的 sync.Pool 中获取,必要时进行扩容,确保返回的缓冲区总是满足需求。
  6. 精确的缓冲区回收Put 方法根据缓冲区的实际容量将其归还到正确的 sync.Pool 中,保证了缓冲区被高效重用。
  7. 优雅的边界处理:对于超出管理范围的大容量请求,直接创建新的缓冲区而不是从池中获取,避免了池管理过大的缓冲区。

这个设计的优势在于:

  • 内存效率:通过精确的容量管理和 2 的幂次划分,显著减少了内存碎片和浪费。
  • 性能优化:预分配不同大小的缓冲区,减少了运行时的内存分配,提高了系统响应速度。
  • 扩展性:可以根据实际需求轻松调整容量范围,适应不同的应用场景。
  • 计算效率:利用位操作和 2 的幂次特性,提高了各种操作的计算效率。

通过这种设计,GWS 在处理大量并发的 WebSocket 连接时,能够高效地管理内存,减少垃圾回收的压力,从而提供更稳定和高效的性能。这个 bytes.Buffer 池的实现展示了如何在实际项目中权衡和优化内存使用,是一个值得学习和借鉴的优秀范例。

TIPS:个人心得,仅供参考

  1. 谦虚学习:技术的世界瞬息万变,每天都有新的知识和技术涌现。保持谦虚的态度,虚心向他人学习,才能不断进步。
  2. 深入理解:在学习和使用技术时,不仅要知其然,更要知其所以然。深入理解底层原理和机制,才能在遇到问题时游刃有余。
  3. 实践出真知:理论知识固然重要,但实践更能检验和巩固所学。多动手实践,通过实际项目积累经验,才能真正掌握技术。
  4. 分享与交流:技术的进步离不开分享与交流。将自己的心得体会分享给他人,不仅能帮助他人进步,也能在交流中获得新的启发。
  5. 持续改进:技术没有止境,永远都有改进的空间。保持对技术的热爱和追求,不断优化和提升自己的代码和设计。

最后,感谢大家的阅读和支持。希望这篇文章能对你有所启发和帮助。如果有任何问题或建议,欢迎随时交流探讨。

转载链接:https://juejin.cn/post/7413564131309715471


Tags:


本篇评论 —— 揽流光,涤眉霜,清露烈酒一口话苍茫。


    声明:参照站内规则,不文明言论将会删除,谢谢合作。


      最新评论




ABOUT ME

Blogger:袅袅牧童 | Arkin

Ido:PHP攻城狮

WeChat:nnmutong

Email:nnmutong@icloud.com

标签云