date: 2025-10-19 15:54
title: ArrayPool的底层原理
category: dotnet
tag:
- memory
description: 探究ArrayPool的底层原理
ArrayPool的简单使用
var pool = ArrayPool<byte>.Shared;
var bytes = pool.Rent(1024);
....
pool.Return(bytes);
- 文中所研究的的
ArrayPool为 .net 自带的SharedArrayPool - 可以将ArrayPool看成是一个黑盒子,需要内存的时候去向黑盒子借一些,使用完毕之后再还给它,旨在减少向操作系统申请内存的次数,也减少了GC回收的次数。亦可以理解为缓存吧。
ArrayPool底层原理
-
ArrayPool中分配了 27个桶,每个桶都对应不同的尺寸,分别是
16、32、64、128、256...1G- 他们之间的关系是
x 2,最小的是16个字节,最大的是1024 * 1024 * 1024也就是一个 G,如果超出了这个范围,那么就不会缓存。
- 他们之间的关系是
-
每次
Rent的时候先看桶中有没有闲置的Buffer,如果有的话直接返回Buffer,否则需要申请内存。需要注意的是,返回的数组大小>=申请的大小 -
Return的时候将Buffer放到对应的桶中,方便下次借用的时候直接拿过来用。 -
notes:
-
bucket是ThreadLocal的,不能跨线程
-
可以不在
Rent的前提下直接Return,前提是长度有效。- 有效长度为:
[2^4, 2^30]且为2的倍数
- 有效长度为:
-
用完一定要记得
Return,否则与直接new无异(且有空间损耗)
-
简化过的源码
class SharedArrayPool<T> : ArrayPool<T>
{
private const int NumBuckets = 27;
[ThreadStatic]
private static SharedArrayPoolThreadLocalArray[]? t_tlsBuckets;
public override T[] Rent(int minimumLength)
{
T[]? buffer;
// calculate bucket index
int bucketIndex = Utilities.SelectBucketIndex(minimumLength);
SharedArrayPoolThreadLocalArray[]? tlsBuckets = t_tlsBuckets;
if (tlsBuckets is not null && (uint)bucketIndex < (uint)tlsBuckets.Length)
{
buffer = Unsafe.As<T[]>(tlsBuckets[bucketIndex].Array);
if (buffer is not null)
{
// get from the bucket
tlsBuckets[bucketIndex].Array = null;
return buffer;
}
}
// resize the allocate size
minimumLength = ...;
// allocate memory
buffer = GC.AllocateUninitializedArray<T>(minimumLength);
return buffer;
}
public override void Return(T[] array, bool clearArray = false)
{
int bucketIndex = Utilities.SelectBucketIndex(array.Length);
if ((uint)bucketIndex < (uint)tlsBuckets.Length)
{
// Check to see if the buffer is the correct size for this bucket.
if (array.Length != Utilities.GetMaxSizeForBucket(bucketIndex))
{
throw new ArgumentException(SR.ArgumentException_BufferNotFromPool, nameof(array));
}
ref SharedArrayPoolThreadLocalArray tla = ref tlsBuckets[bucketIndex];
Array? prev = tla.Array;
// put the array into bucket
tla = new SharedArrayPoolThreadLocalArray(array);
}
}
}
验证Return缓存
var pool = ArrayPool<byte>.Shared;
var bucketsField = pool.GetType().GetField("t_tlsBuckets", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
if (bucketsField == null) return;
var buckets = bucketsField.GetValue(pool);
var bytes = pool.Rent(100 * 1024 * 1024);
pool.Return(bytes);
- 运行测试代码,在
Rent行处打上断点,发现buckets27个均没有Array,等Return之后再看buckets发现23处的bucket的Array已有实际内容。
验证缓存命中
long before = GC.GetAllocatedBytesForCurrentThread();
var buffer = ArrayPool<byte>.Shared.Rent(1024);
long after = GC.GetAllocatedBytesForCurrentThread();
var allocateSize = after - before;
if (allocateSize > 0)
Debug.WriteLine("申请内存");
else
Debug.WriteLine("命中缓存");
- 在
Rent前后分别打印一下当前线程GC所分配的内存的大小,判定是 "申请内存" 还是 "命中缓存"
验证超过1GB的数组不会缓存
方法一:使用 GC.GetAllocatedBytesForCurrentThread() 方法
- 略
方法二:使用vs调试查看bucket array
- 在
Return之后查看27个缓存中有没有超过1GB的数组,可以通过vs调试直接看
方法三:windbg查看 Return 执行过程
var pool = ArrayPool<byte>.Shared;
var bytes = pool.Rent(1025 * 1024 * 1024);
pool.Return(bytes);
- 可以看到,将
1025mb的数组Return时,bucketIndex是27,已经超出了缓存buckets的长度,所以相当于Return方法什么都没有做。
验证不Rent,直接Return一个数组是否会缓存
-
看数组的大小,如果长度有效则缓存,如果长度无效则抛出异常
-
如果长度为128,正好符合缓存bucket的大小,所以能够缓存,没有任何报错
-
如果是129字节,那么在
Return的时候会报错.- 查看源代码后发现是有一处大小校验,确保缓存的大小必须符合
bucket的大小规则([2^4, 2^30]且是2的整数倍)
验证缓存的ThreadLocal性
var pool = ArrayPool<byte>.Shared;
var bytes = new byte[128];
bytes[0] = 1;
bytes[1] = 2;
bytes[2] = 3;
pool.Return(bytes);
await Task.Run(() =>
{
var pool2 = ArrayPool<byte>.Shared;
var ret = pool2.Rent(128);
Debug.WriteLine((ret == bytes).ToString());
});
- 如上代码所示,先在主线程缓存了一个128长度的数组,再开了一个线程去
Rent 128,你将会发现并没有命中缓存。
Q & A
- ArrayPool最大支持多大的缓存?
var bytes = ArrayPool<byte>.Shared.Rent(65)返回的bytes长度为多少?- 如果先在线程A中缓存了一个
128长度的byte[],那么随后在线程B中Rent(128)能够命中缓存吗? - 如果光
Rent而不Return会出现什么样的后果?
references
扩展
- 如果在已经存在缓存的情况下,调用
Return,old buffer哪去了?