.Net性能調優-MemoryPool

簡單用法

//獲取MemoryPool實例,實際返回了一個ArrayMemoryPool<T>
MemoryPool<char> Pool = MemoryPool<char>.Shared;

//加上using
using IMemoryOwner<char> owner = Pool.Rent(1024 * 900);

Memory<char> memory = owner.Memory;
for (int i = 0; i < 1024 * 900; i++)
{
	memory.Span[i] = 'x';
}

//從100的下標開始截取10個字元
var sliceArr = memory.Span.Slice(100, 10).ToArray();

ArrayMemoryPool<T>

通過MemoryPool<int>.Shared我們可以獲取到一個MemoryPool<T>的示例,該實例的類型為ArrayMemoryPool<T>

ArrayMemoryPool<T>實際上只有一個函數可用,就是Rent(),還有一個Dispose()函數但是裡面沒有任何程式碼

Rent()限制了最大租借長度:2147483647u=(1024^3*2-1)=(2G-1bytes),並返回一個IMemoryOwner<T>對象

ArrayMemoryPoolBuffer<T>

ArrayMemoryPool<T>Rent()實際返回了一個ArrayMemoryPoolBuffer<T>,該類繼承了IMemoryOwner<T>

它提供一個Memroy屬性來獲取一個Memory<T>對象,我們可以藉助Memroy<T>來操控我們租用的數組了

ArrayMemoryPool<T>的內部實際還是調用的ArrayPool<T>.Shared 來租用數組

實現程式碼如下:

public Memory<T> Memory
{
    get
    {
        T[] array = _array;
        if (array == null)
        {
            System.ThrowHelper.ThrowObjectDisposedException_ArrayMemoryPoolBuffer();
        }	
        return new Memory<T>(array);
    }
}

public ArrayMemoryPoolBuffer(int size)
{
    _array = ArrayPool<T>.Shared.Rent(size);
}

public void Dispose()
{
    T[] array = _array;
    if (array != null)
    {
        _array = null;
        ArrayPool<T>.Shared.Return(array);
    }
}

Memory<T>

Memory<T>的構造函數接收一個array<T>,存在私有變數_object中。Memory中對數組的操作最終又依賴於Span<T>

public readonly struct Memory<T> : IEquatable<Memory<T>>
{
	private readonly object _object;

	private readonly int _index;

	private readonly int _length;

	public static Memory<T> Empty => default(Memory<T>);

	public int Length => _length;

	public bool IsEmpty => _length == 0;

	public unsafe Span<T> Span=>{ _object...}
	
	public T[] ToArray()
	{
		return Span.ToArray();
	}
}

Slice(start,length)

public Memory<T> Slice(int start)
{
    return new Memory<T>(_object, _index + start, _length - start);
}

Memory的Slice函數可以對數組進行截取,該函數仍然返回一個Memory<T>對象,新的對象記錄了原始的_object和要切割的indexlength,所以該函數不會造成額外的記憶體消耗

Pin()

該函數的作用是獲取數組記憶體的管理權,不讓垃圾回收器回收,自己管理記憶體,但他怎麼自己管理的,暫時木有研究。。。有興趣的小夥伴可以自行研究。

但是因為我們的數組是從ArrayPool<T>租借的,由ArrayMemoryPool<T>Dispose函數回收的,所以本文對於Memory的使用方式中並不會用到該函數

所以綜上所述,簡單的理解就是Memory<T>主要是用來管理Span<T>

那麼Span又是啥呢?為什麼Memrory<T>不直接提供操作數組的方式,而是要返回一個Span<T>呢?

啊哈哈,這個就問倒我了

簡單地概括其原因就是通過Span<T>來操作數組切割分片賦值等,更快,更節約記憶體,數據流轉無複製。而Span本身過於底層,使用方面有很多的局限性。所以最終改用Memory<T>來控制Span<T>的生命周期,而只給用戶提供Span的操作數組的相關函數。

至於它怎麼底層了,局限性在哪些方面,想深入研究的話網上有很多相關的優秀文章值得閱讀一下。對於Span的學習我現在是適可而止,後面把記憶體需要學習的相關知識學的差不多的時候再回來研究它吧,欠的債早晚是要還的啊…

使用場景

啊這…,對於MemoryPool,確實沒有想到使用場景。因為之前的文章曾經介紹過ArrayPool。而MemoryPool的數組又是依賴ArrayPool創建的,反而相比於ArrayPool他又多了創建IMemoryOwner的消耗。這裡推薦一篇文章對二者進行了對比://endjin.com/blog/2020/09/arraypool-vs-memorypool-minimizing-allocations-ais-dotnet。

其實我們更需要的是Memory<T>和Span<T>,在System.Memory命名空間下的MemoryExtensions為我們的Array[]提供了擴展方法AsMemory\<T\>(this T[]? array) AsSpan\<T\>(this T[]? array)等等幾十種擴展,足夠滿足你的需要

所以如果不是非得要用MemoryPool的場景還是推薦直接使用ArrayPool吧,當然如果你有必須使用MemoryPool的場景還請在下面留言告訴我一下是什麼場景,互相學習一下,嘿嘿