1: Background
1. Tell a story
Recently, in the process of analyzing a dump, I found that there are many large free arrays on gen2 and LOH. After a careful look, most of these free arrays were byte [] arrays of html fragments generated by the template engine. Of course, this article is not to analyze dump, but to talk about how to make more efficient memory utilization when there are many byte [] arrays with large length in the pipe stack, How to make gc old man less stressed.
I don't know if you've found it netcore has added many pooled objects, such as ArrayPool, ObjectPool, etc., which are really very practical in some scenarios, so it is necessary to have a deeper understanding of them.
2: ArrayPool source code analysis
1. A picture is worth a thousand words
After I spent nearly an hour reading the source code, I drew a pool diagram of ArrayPool. The so-called: I have one picture in hand and I have it in the world.
With this picture, I'll talk about some concepts and the corresponding source code. I think it should be almost the same.
2. What is the architecture classification of pooling?
ArrayPool is composed of several buckets, and the Bucket is composed of several buffer [] arrays. With this concept, you can configure the code.
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 array }
3. Why is there 50 buffer s in each bucket []
This question is easy to answer. maxArraysPerBucket=50 is set during initialization. Of course, you can also customize it. Refer to the following code for details:
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. Buffer [] Why is the length 16, 32, 64
The framework assumes by default that buffer [] Length = 16, buffer [] Length is x2 cumulative. The code involved is getmaxsizefor bucket() method. Refer to the following:
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. How many bucket s are there during initialization?
In fact, in the figure above, I don't give how many bucket s there are, and how many are there? 😓😓😓 , After I read the source code, the algorithm is very interesting.
Let's talk about the results first. There are 17 bucket s by default. You will be curious about how to calculate them? Let's start with the following two variables:
Maxarraylength = 1048576 = the 20th power of 2
buffer. Length = 16 = the 4th power of 2
The final algorithm is to take the difference to the power: bucket [] Length = 20 - 4 + 1 = 17, in other words, the buffer [] Length = 1048576. Please refer to SelectBucketIndex() method for details.
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; } }
Here, I believe you have understood the idea of the pooled architecture of ArrayPool. Next, let's see how to apply for and return buffer [].
3: How to apply for and return
Since buffer [] has been granulated, it should be borrowed and returned. The response to the code is the Rent() and Return() methods. To facilitate understanding, the code says:
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(); } }
Once you have the code and diagram, go through the process a little bit.
- Borrow an array with the size of byte[10] from the ArrayPool. In order to save memory, do not stock goods first, and temporarily generate a byte [] Size = 16. The simplified code is as follows. Refer to 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; }
There is a hole here. You think you borrowed byte[10], but the reality is byte[16]. Pay attention here.
- When using arraypool When returning byte[16], it is obvious that it falls on the first buffer [] of the first bucket. Refer to the following simplified code:
internal void Return(T[] array) { if (_index != 0) { _buffers[--_index] = array; } }
There is also a noteworthy pit here, that is, the data in the returned byte[16] will not be cleared by default. It can be seen from the above code. To clean up, you need to specify clearArray=true in the Return method. Refer to the following code:
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); } }
4: Summary
Learning the idea of pooling architecture can provide some inspiration for project development at ordinary times. Secondly, pooling is a very good method for those scenes with one-time byte [], which is also an optimization idea I put forward after analyzing my friend dump.
More high quality dry goods: see my GitHub: dotnetfly