如何构建高效的字节数组池?
摘要:.NET利用ArrayPoolPool<T>和MemoryPool<T>提供了针对ArrayMemory<T&g
.NET利用ArrayPoolPool<T>和MemoryPool<T>提供了针对Array/Memory<T>的对象池功能。最近在一个项目中需要使用到针对字节数组的对象池,由于这些池化的字节数组相当庞大,我希望将它们分配到POH上以降低GC的压力。由于ArrayPoolPool<T>没法提供支持,所以我提供了一个极简的实现。
目录
一、Bucket
二、ByteArrayOwner
三、ByteArrayPool
四、测试
一、Bucket和大部分实现方案一样,我需要限制池化数组的最大尺寸,同时设置最小长度为16。我们将[16-MaxLength]划分为N个区间,每个区间对应一个Bucket,该Bucket用来管理“所在长度区间”的字节数组。如下所示的就是这个Bucket类型的定义:我们利用一个ConcurrentBag<byte[]>来维护池化的字节数组,数组的“借”与“还”由TryTake和Add方法来实现。
internal sealed class Bucket
{
private readonly ConcurrentBag<byte[]> _byteArrays = new();
public void Add(byte[] array) => _byteArrays.Add(array);
public bool TryTake([MaybeNullWhen(false)] out byte[] array) => _byteArrays.TryTake(out array);
}二、ByteArrayOwner 从对象池“借出”的是一个ByteArrayOwner 对象,它是对字节数组和所在Bucket的封装。如果指定的数组长度超过设置的阈值,意味着Bucket不存在,借出的字节数组也不需要还回去,这一逻辑体现在IsPooled属性上。ByteArrayOwner 实现了IDisposable接口,实现Dispose方法调用Bucket的Add方法完成了针对字节数组的“归还”,该方法利用针对_isReleased字段的CompareExchange操作解决“重复归还”的问题。
public sealed class ByteArrayOwner : IDisposable
{
private readonly byte[] _bytes;
private readonly Bucket? _bucket;
private volatile int _isReleased;
public bool IsPooled => _bucket is not null;
internal ByteArrayOwner(byte[] bytes, Bucket? bucket)
{
_bytes = bytes;
_bucket = bucket;
}
public byte[] Bytes => _isReleased == 0 ? _bytes : throw new ObjectDisposedException("The ByteArrayOwner has been released.");
public void Dispose()
{
if (Interlocked.CompareExchange(ref _isReleased, 1, 0) == 0)
{
_bucket?.Add(_bytes);
}
}
}三、ByteArrayPool具体的对象池实现体现在如下所示的ByteArrayPool类型上,池化数组的最大长度在构造函数中指定,ByteArrayPool据此划分长度区间并创建一组通过_buckets字段表示的Bucket数组。具体的区间划分实现在静态方法SelectBucketIndex方法中,当我们根据指定的数组长度确定具体Bucket的时候(对于Bucket在_buckets数组中的索引)同样调用此方法。另一个静态方法GetMaxSizeForBucket执行相反的操作,它根据指定的Bucket索引计算长度区间的最大值。当某个Bucket确定后,得到的数组都具有这个长度。作为ArrayPoolPool<T>的默认实现,ConfigurableArrayPool<T>也采用一样的算法。
