.Net效能調優-MemoryPool

張三~~發表於2021-09-14

簡單用法

//獲取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的消耗。這裡推薦一篇文章對二者進行了對比:https://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的場景還請在下面留言告訴我一下是什麼場景,互相學習一下,嘿嘿

相關文章