关于.net:ArrayPool-源码解读之-byte-也能池化

8次阅读

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


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

  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

正文完
 0