共计 3691 个字符,预计需要花费 10 分钟才能阅读完成。
一:背景
1. 讲故事
最近在剖析一个 dump 的过程中发现其在 gen2 和 LOH 上有不少 size 较大的 free,认真看了下,这些 free 生前大多都是模板引擎生成的 html 片段的 byte[] 数组,当然这篇我不是来剖析 dump 的,而是来聊一下,当托管堆有很多 length 较大的 byte[] 数组时,如何让内存利用更高效,如何让 gc 老先生压力更小。
不晓得大家有没有发现在 .netcore 中减少了不少池化对象的货色,比方:ArrayPool,ObjectPool 等等,的确在某些场景下还是特地实用的,所以有必要对其进行较深刻的了解。
二:ArrayPool 源码剖析
1. 一图胜千言
在我花了将近一个小时的源码浏览之后,我画了一张 ArrayPool 的池化图,所谓: 一图在手, 天下我有
。
有了这张图,接下来再聊几个概念并配上相应源码,我感觉应该就差不多了。
2. 池化的架构分级是什么样的?
ArrayPool 是由若干个 Bucket 组成,而 Bucket 又由若干个 buffer[]
数组组成, 有了这个概念之后,再配一下代码。
public abstract class ArrayPool<T>
{public static ArrayPool<T> Create()
{return new ConfigurableArrayPool<T>();
}
}
internal sealed class ConfigurableArrayPool<T> : ArrayPool<T>
{
private sealed class Bucket
{
internal readonly int _bufferLength;
private readonly T[][] _buffers;
private int _index;
}
private readonly Bucket[] _buckets; //bucket 数组}
3. 为什么每一个 bucket 里都有 50 个 buffer[]
这个问题很好答复,初始化时做了 maxArraysPerBucket=50
设定,当然你也能够自定义,具体参考如下代码:
internal sealed class ConfigurableArrayPool<T> : ArrayPool<T>
{internal ConfigurableArrayPool() : this(1048576, 50)
{ }
internal ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket)
{int num = Utilities.SelectBucketIndex(maxArrayLength);
Bucket[] array = new Bucket[num + 1];
for (int i = 0; i < array.Length; i++)
{array[i] = new Bucket(Utilities.GetMaxSizeForBucket(i), maxArraysPerBucket, id);
}
_buckets = array;
}
}
4. bucket 中 buffer[].length 为什么顺次是 16,32,64 …
框架做了默认假设,第一个 bucket 中的 buffer[].length=16
, 后续 bucket 中的 buffer[].length
都是 x2 累计,波及到代码就是 GetMaxSizeForBucket()
办法,参考如下:
internal ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket)
{Bucket[] array = new Bucket[num + 1];
for (int i = 0; i < array.Length; i++)
{array[i] = new Bucket(Utilities.GetMaxSizeForBucket(i), maxArraysPerBucket, id);
}
}
internal static int GetMaxSizeForBucket(int binIndex)
{return 16 << binIndex;}
5. 初始化时 bucket 到底有多少个?
其实在上图中我也没有给出 bucket 到底有多少个,那到底是多少个呢?😓😓😓,当我浏览完源码之后,这算法还挺有意思的。
先说一下后果吧,默认 17 个 bucket,你必定会好奇怎么算的?先说下两个变量:
- maxArrayLength=1048576 = 2 的 20 次方
- buffer.length= 16 = 2 的 4 次方
最初的算法就是取次方的差值:bucket[].length= 20 - 4 + 1 = 17
,换句话说最初一个 bucket 下的 buffer[].length=1048576
,具体代码请参考 SelectBucketIndex()
办法。
internal sealed class ConfigurableArrayPool<T> : ArrayPool<T>
{internal ConfigurableArrayPool(): this(1048576, 50)
{ }
internal ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket)
{int num = Utilities.SelectBucketIndex(maxArrayLength);
Bucket[] array = new Bucket[num + 1];
for (int i = 0; i < array.Length; i++)
{array[i] = new Bucket(Utilities.GetMaxSizeForBucket(i), maxArraysPerBucket, id);
}
_buckets = array;
}
internal static int SelectBucketIndex(int bufferSize)
{return BitOperations.Log2((uint)(bufferSize - 1) | 0xFu) - 3;
}
}
到这里我置信你对 ArrayPool 的池化架构思路曾经搞明确了,接下来看下如何申请和偿还 buffer[]。
三:如何申请和偿还
既然 buffer[] 做了颗粒化,那就应该好借好还,反馈到代码上就是 Rent()
和 Return()
办法,为了不便了解,上代码谈话:
class Program
{static void Main(string[] args)
{var arrayPool = ArrayPool<int>.Create();
var bytes = arrayPool.Rent(10);
for (int i = 0; i < bytes.Length; i++) bytes[i] = 10;
arrayPool.Return(bytes);
Console.ReadLine();}
}
有了代码和图之后,再略微捋一下流程。
- 从 ArrayPool 中借一个
byte[10]
大小的数组,为了节俭内存,先不备货,长期生成一个byte[].size=16
的数组进去,简化后的代码如下,参考if (flag)
处:
internal T[] Rent()
{T[][] buffers = _buffers;
T[] array = null;
bool lockTaken = false;
bool flag = false;
try
{if (_index < buffers.Length)
{array = buffers[_index];
buffers[_index++] = null;
flag = array == null;
}
}
if (flag)
{array = new T[_bufferLength];
}
return array;
}
这里有一个坑,那就是你认为借了 byte[10]
,事实给你的是 byte[16]
,这里略微留神一下。
- 当用 ArrayPool.Return 偿还
byte[16]
时, 很显著看到它落到了第一个 bucket 的第一个 buffer[] 上,参考如下简化后的代码:
internal void Return(T[] array)
{if (_index != 0)
{_buffers[--_index] = array;
}
}
这里也有一个值得注意的坑,那就是还回去的 byte[16]
外面的数据默认是不会清掉的,从下面的代码也是能够看进去的,要想做清理,须要在 Return 办法中指定 clearArray=true
,参考如下代码:
public override void Return(T[] array, bool clearArray = false)
{int num = Utilities.SelectBucketIndex(array.Length);
if (num < _buckets.Length)
{if (clearArray)
{Array.Clear(array, 0, array.Length);
}
_buckets[num].Return(array);
}
}
四:总结
学习这其中的 池化架构
思维,对平时我的项目开发还是能提供一些灵感的,其次对那些一次性应用 byte[]
的场景,用池化是个十分不错的办法,这也是我对敌人 dump 剖析后提出的一个优化思路。
更多高质量干货:参见我的 GitHub: dotnetfly