一:背景

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();        }    }


有了代码和图之后,再略微捋一下流程。

  1. 从 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],这里略微留神一下。

  1. 当用 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