共用方式為


MemoryOwner<T>

MemoryOwner<T>是實作 IMemoryOwner<T>、內嵌長度屬性和一系列效能導向 API 的緩衝區類型。 它基本上是一個輕量型包裝函式, ArrayPool<T> 其中包含一些額外的協助程式公用程式。

平臺 API:MemoryOwner<T>AllocationMode

運作方式

MemoryOwner<T> 具有下列主要功能:

  • API 所傳回陣列和 IMemoryOwner<T> API 所傳回ArrayPool<T>實例的其中一個主要問題,就是使用者指定的大小只是做為最小大小:傳回MemoryPool<T>緩衝區的實際大小實際上可能更大。 MemoryOwner<T> 藉由儲存原始要求的大小來解決此問題,因此 Memory<T> 從中擷取的和 Span<T> 實例永遠不需要手動配量。
  • 使用 IMemoryOwner<T>時,取得Span<T>基礎緩衝區的 需要先取得 實體,然後再Span<T>取得 Memory<T> 。 這相當昂貴,而且通常不需要,因為中繼 Memory<T> 可能完全不需要。 MemoryOwner<T> 而是具有非常輕量的額外 Span 屬性,因為它會直接包裝從集區租用的內部 T[] 陣列。
  • 默認不會清除從集區租用的緩衝區,這表示如果先前傳回集區時未清除緩衝區,則可能包含垃圾數據。 通常,用戶必須手動清除這些租用的緩衝區,這可以是詳細資訊,特別是經常完成時。 MemoryOwner<T> 透過 Allocate(int, AllocationMode) API 有更靈活的方法。 這個方法不僅會配置剛好要求大小的新實例,還可以用來指定要使用的配置模式:與 相同的 ArrayPool<T>配置模式,或自動清除租用緩衝區的實例。
  • 在某些情況下,緩衝區的租用大小可能大於實際需要的大小,然後之後重設大小。 這通常需要使用者租用新的緩衝區,並從舊的緩衝區複製感興趣的區域。 相反地,公開 Slice(int, int) API,MemoryOwner<T>只傳回包裝指定感興趣區域的新實例。 這可讓您略過租用新的緩衝區,並完全複製專案。

語法

以下是如何租用緩衝區並擷取 Memory<T> 實例的範例:

// Be sure to include this using at the top of the file:
using Microsoft.Toolkit.HighPerformance.Buffers;

using (MemoryOwner<int> buffer = MemoryOwner<int>.Allocate(42))
{
    // Both memory and span have exactly 42 items
    Memory<int> memory = buffer.Memory;
    Span<int> span = buffer.Span;

    // Writing to the span modifies the underlying buffer
    span[0] = 42;
}

在此範例中,我們使用 區塊 using 來宣告 MemoryOwner<T> 緩衝區:這特別有用,因為基礎陣列會自動傳回區塊結尾的集區。 如果我們沒有直接控制實例的 MemoryOwner<T> 存留期,則當垃圾收集行程完成物件時,緩衝區會直接傳回集區。 在這兩種情況下,租用的緩衝區一律會正確地傳回至共用集區。

何時應該使用此功能?

MemoryOwner<T> 可作為一般用途緩衝區類型,其優點是將一段時間內完成的配置數目降到最低,因為它會在內部重複使用共用集區中的相同數位。 常見的使用案例是取代 new T[] 數位配置,特別是在執行重複作業時,需要暫存緩衝區才能運作,或產生緩衝區的結果。

假設我們有一個由一系列二進位檔所組成的數據集,而且我們需要讀取所有這些檔案,並以某種方式處理它們。 為了正確分隔程式代碼,我們最終可能會撰寫一個唯讀取一個二進位檔的方法,如下所示:

public static byte[] GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    byte[] buffer = new byte[(int)stream.Length];

    stream.Read(buffer, 0, buffer.Length);

    return buffer;
}

請注意, new byte[] 表達式。 如果我們讀取大量的檔案,我們最終會配置許多新的陣列,這將給垃圾收集行程帶來了很大的壓力。 我們可能想要使用從集區租用的緩衝區來重構此程序代碼,如下所示:

public static (byte[] Buffer, int Length) GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    byte[] buffer = ArrayPool<T>.Shared.Rent((int)stream.Length);

    stream.Read(buffer, 0, (int)stream.Length);

    return (buffer, (int)stream.Length);
}

使用此方法時,緩衝區現在會從集區租用,這表示在大部分情況下,我們都可以略過配置。 此外,由於預設不會清除租用的緩衝區,因此我們也可以節省用零填滿緩衝區所需的時間,這可讓我們獲得另一個小的效能改善。 在上述範例中,載入 1000 個檔案會將總配置大小從大約 1 MB 減少到只有 1024 個字節,而只會有效地配置單一緩衝區,然後自動重複使用。

上述程式代碼有兩個主要問題:

  • ArrayPool<T> 可能會傳回大小大於所要求緩衝區的緩衝區。 若要解決此問題,我們需要傳回 Tuple,此 Tuple 也表示實際使用的大小放入我們租用的緩衝區中。
  • 只要傳回數位,我們必須特別小心,才能正確追蹤其存留期,並將其傳回適當的集區。 我們可能會改用 MemoryPool<T> ,並傳回 IMemoryOwner<T> 實例來解決此問題,但我們仍有租用緩衝區的大小大於我們需要的問題。 此外,IMemoryOwner<T>擷取 Span<T> 時會有一些額外負荷,因為其為介面,而且我們一律需要先取得實例,然後再Span<T>取得 Memory<T>

若要解決這兩個問題,我們可以使用 再次 MemoryOwner<T>重構此程序代碼:

public static MemoryOwner<byte> GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    MemoryOwner<byte> buffer = MemoryOwner<byte>.Allocate((int)stream.Length);

    stream.Read(buffer.Span);

    return buffer;
}

傳回的 IMemoryOwner<byte> 實例會負責處置基礎緩衝區,並在叫用其 IDisposable.Dispose 方法時將它傳回集區。 我們可以使用它來取得 Memory<T>Span<T> 實例來與載入的數據互動,然後在不再需要時處置實例。 此外,所有 MemoryOwner<T> 屬性(例如 MemoryOwner<T>.Span)都遵守我們所使用的初始要求大小,因此我們不再需要手動追蹤租用緩衝區內的有效大小。

範例

您可以在單元測試中找到更多範例。