1 private static readonly SharedArrayPool<T> s_shared = new SharedArrayPool<T>();
2
3 public static ArrayPool<T> Shared => s_shared;
4
5 public static ArrayPool<T> Create() => new ConfigurableArrayPool<T>();
从以上ArrayPool的初始化代码可以发现,其数组对象池的创建是由ConfigurableArrayPool类完成的,那么我们继续看一下对应的初始化逻辑。部分代码已经做过删减,我们只关注核心的实现逻辑,需要看全部的实现代码的同学,可以自行前往GitHub上查看。
1 private const int DefaultMaxArrayLength = 1024 * 1024;
2 private const int DefaultMaxNumberOfArraysPerBucket = 50;
3 private readonly Bucket[] _buckets;
4 internal ConfigurableArrayPool() : this(DefaultMaxArrayLength, DefaultMaxNumberOfArraysPerBucket){ }
5 internal ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket)
6 {
7 ...
8
9 int maxBuckets = Utilities.SelectBucketIndex(maxArrayLength);
10 var buckets = new Bucket[maxBuckets + 1];
11 for (int i = 0; i < buckets.Length; i++)
12 {
13 buckets[i] = new Bucket(Utilities.GetMaxSizeForBucket(i), maxArraysPerBucket, poolId);
14 }
15 _buckets = buckets;
16 }
我们从源码中可以看出几个比较重要的实现逻辑,ConfigurableArrayPool在初始化时,设置了默认的两个参数DefaultMaxArrayLength和DefaultMaxNumberOfArraysPerBucket,分别用于设置默认的池中每个数组的默认最大长度(2^20)和设置每个桶默认可出租的最大数组数。根据传入的参数,对其调用Utilities.SelectBucketIndex(maxArrayLength)进行计算,根据最大数组长度计算出桶的数量 maxBuckets,然后创建一个数组 buckets。
1 internal static int SelectBucketIndex(int bufferSize)
2 {
3 return BitOperations.Log2((uint)bufferSize - 1 | 15) - 3;
4 }
SelectBucketIndex使用位操作和数学运算来确定给定缓冲区大小应分配到哪个桶。该方法的目的是为了根据缓冲区的大小,有效地将缓冲区分配到适当大小的桶中。在bufferSize大小介于 2^(n-1) + 1 和 2^n 之间时,分配大小为 2^n 的缓冲区。使用了BitOperations.Log2 方法,计算 (bufferSize - 1) | 15 的二进制对数(以 2 为底)。由于要处理1到16字节之间的缓冲区,使用了|15 来确保范围内的所有值都会变成15。最后,通过-3进行调整,以满足桶索引的需求。针对零大小的缓冲区,将其分配给最高的桶索引,以确保零长度的缓冲区不会由池保留。对于这些情况,池将返回 Array.Empty 单例。
1 internal static int GetMaxSizeForBucket(int binIndex)
2 {
3 int maxSize = 16 << binIndex;
4 return maxSize;
5 }
GetMaxSizeForBucket将数字 16 左移 binIndex 位。因为左移是指数增长的,所以这样的计算方式确保了每个桶的大小是前一个桶大小的两倍。初始桶的索引(binIndex 为 0)对应的最大大小为 16。这种是比较通用的内存管理的策略,按照一系列固定的大小划分内存空间,这样可以减少分配的次数。接下来我们看一下Bucket对象的初始化代码。
1 internal readonly int _bufferLength;
2 private readonly T[]?[] _buffers;
3 private readonly int _poolId;
4 private SpinLock _lock;
5 internal Bucket(int bufferLength, int numberOfBuffers, int poolId)
6 {
7 _lock = new SpinLock(Debugger.IsAttached);
8 _buffers = new T[numberOfBuffers][];
9 _bufferLength = bufferLength;
10 _poolId = poolId;
11 }
SpinLock只有在附加调试器时才启用线程跟踪;它为Enter/Exit增加了不小的开销;numberOfBuffers表示可以租借的次数,只初始化定义个二维的泛型数组,未分配内存空间;bufferLength每个缓冲区的大小。以上的逻辑大家可能不是很直观,我们用一个简单的图给大家展示一下。
1 ArrayPool
2 |
3 +-- Bucket[0] (Buffer Size: 16)
4 | +-- Buffer 1 (Size: 16)
5 | +-- Buffer 2 (Size: 16)
6 | +-- ...
7 |
8 +-- Bucket[1] (Buffer Size: 32)
9 | +-- Buffer 1 (Size: 32)
10 | +-- Buffer 2 (Size: 32)
11 | +-- ...
12 |
13 ...
14 默认会创建50个Buffer
如果对C#的字典的结构比较了解的同学,可能很好理解,ArrayPool是由一个一维数组和一个二维泛型数组进行构建。无论是.NET 还是JAVA中,很多的复杂的数据结构都是由多种简单结构进行组合,这样不仅一定程度上保证数据的取的效率,又可以考虑插入、删除的性能,也兼顾内存的占用情况。这里用一个简单的图来说明一下二维数组的初始化时占用的内存的结构。(_buffers = new T[numberOfBuffers][])
1 +-----------+
2 | arrayInt |
3 +-----------+
4 | [0] | --> [ ] (Possibly null or an actual array)
5 +-----------+
6 | [1] | --> null
7 +-----------+
8 | [2] | --> null
9 +-----------+
10
11 +----------+
12 | arrInt1 |
13 +----------+
14 | | --> [ ] (Possibly null or an actual array)
15 +----------+