Application of. NET Core Object Pool: Extension

  • 2021-12-04 09:47:15
  • OfStack

Directory 1. Pooled collections 2. Pooled StringBuilder 3. ArrayPool < T > 4. MemoryPool < T >

In principle, all reference type objects can be provided through object pool, but it is necessary to weigh whether they are worth using in specific applications. Although object pooling can avoid GC through object reuse, the objects it stores will consume memory, and it is not worthwhile to use object pooling if the frequency of object reuse is small. If the life cycle of a small object is very short, it can be ensured that GC can recycle it in the 0th generation, and such an object is actually not suitable for being placed in the object pool, because the performance of the 0th generation GC is actually very high. In addition, objects may be extracted by other threads after being released into the object pool. If the release timing is wrong, it may cause multiple threads to operate on the same object at the same time. In a word, we should consider whether the current scenario is applicable to the object pool before using it, and strictly follow the principles of "borrowing and paying back" and "paying back only when not using it".

1. Pooled collections

We know 1 List < T > An array is used inside the object to hold the list elements. The array is fixed length, so List < T > There is a maximum capacity (embodied in its Capacity attribute). When the number of list elements exceeds the array capacity, the list object must be expanded, that is, a new array must be created and the existing elements copied in. The more current elements, the more copy operations that need to be performed, and the natural impact on performance is greater. If we create List < T > Object, and constantly adding objects to it may lead to multiple expansions, so if we can predict the number of elements, we are creating List < T > Object should specify 1 appropriate capacity. But in many cases, the number of list elements changes dynamically, and we can use object pooling to solve this problem.

Next, we use a simple example to demonstrate how to provide an List in the way of object pool under 1 < Foobar > Object with element type Foobar as shown below. To explicitly control the creation and return of list objects, we customize the following FoobarListPolicy, which represents the pooled object policy. Through the introduction of the default implementation of object pool in "Design", we know that we directly inherit PooledObjectPolicy < T > Type ratio implements IPooledObjectPolicy < T > Interface has better performance advantages.


public class FoobarListPolicy : PooledObjectPolicy<List<Foobar>>
{
    private readonly int _initCapacity;
    private readonly int _maxCapacity;

    public FoobarListPolicy(int initCapacity, int maxCapacity)
    {
        _initCapacity = initCapacity;
        _maxCapacity = maxCapacity;
    }
    public override List<Foobar> Create() => new List<Foobar>(_initCapacity);
    public override bool Return(List<Foobar> obj)
   {
        if(obj.Capacity <= _maxCapacity)
        {
            obj.Clear();
            return true;
        }        
        return false;
    }
}

public class Foobar
{
    public int Foo { get; }
    public int Bar { get; }
    public Foobar(int foo, int bar)
    {
        Foo = foo;
        Bar = bar;
    }
}

As shown in the code snippet, we defined two fields in the FoobarListPolicy type, the _ initCapacity field representing the initial capacity specified when the list was created, and the other _ maxCapacity representing the maximum capacity of the object pool storage list. The reason for limiting the maximum size of a list is to avoid memory-resident large lists with little chance of reuse. In the implementation of the Create method, we create the List using the initial capacity < Foobar > Object. In the Return method, we first empty the list to be regressed, and then decide whether to release it into the object pool according to its current capacity. The following program demonstrates the use of object pooling to provide List < Foobar > List. As shown in the code snippet, we are calling Create of the ObjectPoolProvider object < T > Create an ObjectPool that represents the object pool < T > Object, the FoobarListPolicy object is specified as the pooled object policy. We set the initial and maximum capacity to 1K (1024) and 1M (1024*1024). We provide an List with an object pool < Foobar > Object and add 10000 elements to it. If this code is executed frequently, the overall performance will be improved.


class Program
{
    static void Main()
    {
        var objectPool = new ServiceCollection()
            .AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
            .BuildServiceProvider()
            .GetRequiredService<ObjectPoolProvider>()
            .Create(new FoobarListPolicy(1024, 1024*1024));

        string json;
        var list = objectPool.Get();
        try
        {
            list.AddRange(Enumerable.Range(1, 1000).Select(it => new Foobar(it, it)));
            json = JsonConvert.SerializeObject(list);
        }
        finally
        {
            objectPool.Return(list);
        }
    }
}

2. Pooled StringBuilder

We know that if operations on string splicing are frequently involved, StringBuilder should be used for better performance. In fact, StringBuilder objects have their own expansion problems similar to list objects, so the best way is to reuse them by using object pools. The object pooling framework provides native support for pooling StringBuilder objects, and we will demonstrate the specific usage with a simple example.


class Program
{
    static void Main()
    {
        var objectPool = new ServiceCollection()
            .AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
            .BuildServiceProvider()
            .GetRequiredService<ObjectPoolProvider>()
            .CreateStringBuilderPool(1024, 1024*1024);

        var builder = objectPool.Get();
        try
        {
            for (int index = 0; index < 100; index++)
            {
                builder.Append(index);
            }
            Console.WriteLine(builder);
        }
        finally
        {
            objectPool.Return(builder);
        }
    }
}

As shown in the above code snippet, we can directly call the CreateStringBuilderPool extension method of ObjectPoolProvider to get the object pool object (type ObjectPool) for StringBuilder < StringBuilder > ). We demonstrated example 1 above, and we also specified the initial and maximum capacity of the StringBuilder object. The core of pooling the StringBuilder object is embodied in the corresponding policy type, namely the following StringBuilderPooledObjectPolicy type.


public class StringBuilderPooledObjectPolicy : PooledObjectPolicy<StringBuilder>
{
    public int InitialCapacity { get; set; } = 100;
    public int MaximumRetainedCapacity { get; set; } = 4 * 1024;

    public override StringBuilder Create()=> new StringBuilder(InitialCapacity);
    public override bool Return(StringBuilder obj)
    {
        if (obj.Capacity > MaximumRetainedCapacity)
        {
            return false;
        }
        obj.Clear();
        return true;
    }
}

It can be seen that its definition is exactly the same as the FoobarListPolicy type we defined earlier. By default, the initialization and maximum capacity of a pooled StringBuilder object are 100 and 5096, respectively. The following is the ObjectPoolProvider used to create the ObjectPool < StringBuilder > Definitions of two CreateStringBuilderPool extension methods for the.


public static class ObjectPoolProviderExtensions
{
    public static ObjectPool<StringBuilder> CreateStringBuilderPool( this ObjectPoolProvider provider)
        => provider.Create(new StringBuilderPooledObjectPolicy());       

    public static ObjectPool<StringBuilder> CreateStringBuilderPool( this ObjectPoolProvider provider, int initialCapacity, int maximumRetainedCapacity)
    {
        var policy = new StringBuilderPooledObjectPolicy()
        {
            InitialCapacity = initialCapacity,
            MaximumRetainedCapacity = maximumRetainedCapacity,
        };
        return provider.Create(policy);
    }
}

3. ArrayPool < T >

What follows has nothing to do with the previous content, but it belongs to our common object pool usage scenarios. We will use a lot of collections when programming, and many collection types (except collections based on linked lists) use an array as internal storage, so there will be the expansion problem mentioned above. If this array is large, it will also cause GC pressure. We have already solved this problem by using the scheme of pooled collection before, but there is actually another solution to this problem.

In many cases, when we need to create an object, we actually need a continuous sequence of objects of a certain length. Suppose we pool array objects. When we need a fixed-length object sequence, we can extract an available array with a length larger than the required length from the pool and intercept the available continuous fragments (1 from scratch). After using it, we don't need to perform any release operation, and we can directly return the array objects to the object pool. This array-based use of object pools can take advantage of ArrayPool < T > To achieve.


public abstract class ArrayPool<T>
{    
    public abstract T[] Rent(int minimumLength);
    public abstract void Return(T[] array, bool clearArray);

    public static ArrayPool<T> Create();
    public static ArrayPool<T> Create(int maxArrayLength, int maxArraysPerBucket);

    public static ArrayPool<T> Shared { get; }
}

As shown in the above code snippet, the abstract type ArrayPool < T > Also provides a method to complete the two basic operations of the object pool, where the Rent method "lends" an array of not less than (but not equal to) the specified length from the object pool, and the array is finally released to the object pool through the Return method. The clearArray parameter of the Return method indicates whether to empty the array before returning it, depending on how we use it for the array. If we need to override the original content every time, there is no need to perform this redundant operation.

We can create an ArrayPool with the static method Create < T > Object. The pooled arrays are not directly stored in the object pool, and multiple arrays with similar lengths are encapsulated in a bucket (Bucket). This has the advantage that the most matching array (larger than and closer to the specified length) can be quickly found according to the specified length when executing the Rent method. The object pool stores a set of Bucket objects, and the larger the allowed array length, the more buckets. The Create method can specify the capacity of each bucket in addition to the maximum allowed length of the array. In addition to calling the static Create method to create an exclusive ArrayPool < T > Object, we can use the static attribute Shared to return a shared ArrayPool within the application scope < T > Object. ArrayPool < T > The following code snippet demonstrates an example of reading a file.


class Program
{
    static async Task Main()
    {
        using var fs = new FileStream("test.txt", FileMode.Open);
        var length = (int)fs.Length;
        var bytes = ArrayPool<byte>.Shared.Rent(length);
        try
        {
            await fs.ReadAsync(bytes, 0, length);
            Console.WriteLine(Encoding.Default.GetString(bytes, 0, length));
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(bytes);
        }
    }
}

4. MemoryPool < T >

Array is an expression of 1 contiguous segment of memory in the managed heap for storing objects of the same kind, while another type Memory < T > It is more widely used because it can represent not only a contiguous segment of managed (Managed) memory, but also a contiguous segment of Native memory, or even thread stack memory. MemoryPool with the following definition < T > For Memory < T > Object pool of type.


public abstract class MemoryPool<T> : IDisposable
{    
    public abstract int MaxBufferSize { get; }
    public static MemoryPool<T> Shared { get; }

    public void Dispose();
    protected abstract void Dispose(bool disposing);
    public abstract IMemoryOwner<T> Rent(int minBufferSize = -1);
}

public interface IMemoryOwner<T> : IDisposable
{
    Memory<T> Memory { get; }
}

MemoryPool < T > And ArrayPool < T > It has a similar definition, such as obtaining the MemoryPool shared globally by the current application through the static attribute Shared < T > Object, lends 1 Memory not less than the specified size from the object pool through the Rent method < T > Object. The difference is that MemoryPool < T > The Rent method of does not directly return an Memory < T > Object, but an IMemoryOwner that encapsulates the object < T > Object. MemoryPool < T > There is also no EN defined to release Memory < T > The Reurn method of the IMemoryOwner < T > Object's Dispose method. If MemoryPool is adopted, < T > For ArrayPool < T > The demo example of can be rewritten into the following form.


class Program
{
    static async Task Main()
    {
        using var fs = new FileStream("test.txt", FileMode.Open);
        var length = (int)fs.Length;
        using (var memoryOwner = MemoryPool<byte>.Shared.Rent(length))
        {
            await fs.ReadAsync(memoryOwner.Memory);
            Console.WriteLine(Encoding.Default.GetString( memoryOwner.Memory.Span.Slice(0,length)));
        }                
    }
}

Application of NET Core Object Pool: Programming

Application of NET Core Object Pool: Design


Related articles: