Sdílet prostřednictvím


MemoryOwner<T>

Jedná se MemoryOwner<T> o typ vyrovnávací paměti implementuje IMemoryOwner<T>vloženou délku vlastnost a řadu rozhraní API orientovaných na výkon. Je to v podstatě jednoduchý obálka kolem ArrayPool<T> typu, s některými dalšími pomocnými nástroji.

Rozhraní API platformy:MemoryOwner<T>, AllocationMode

Jak to funguje

MemoryOwner<T> má následující hlavní funkce:

  • Jedním z hlavních problémů polí vrácených ArrayPool<T> rozhraními API a IMemoryOwner<T> instancí vrácených MemoryPool<T> rozhraními API je, že velikost určená uživatelem se používá pouze jako minimální velikost: skutečná velikost vrácených vyrovnávacích pamětí může být ve skutečnosti větší. MemoryOwner<T> řeší to také uložením původní požadované velikosti, aby Memory<T> a Span<T> instance načtené z ní nikdy nemusely být ručně řezovány.
  • Při použití IMemoryOwner<T>, získání Span<T> základní vyrovnávací paměti vyžaduje nejprve získat Memory<T> instanci a pak .Span<T> To je poměrně nákladné a často zbytečné, protože středně pokročilý Memory<T> může být skutečně potřeba vůbec. MemoryOwner<T> místo toho má další Span vlastnost, která je extrémně jednoduchá, protože přímo zabalí interní T[] pole, které se pronajímá z fondu.
  • Vyrovnávací paměti pronajané z fondu se ve výchozím nastavení nevymazávají, což znamená, že pokud se při předchozím vrácení do fondu nevymazaly, můžou obsahovat data uvolňování paměti. Uživatelé obvykle musí tyto pronajaté vyrovnávací paměti vymazat ručně, což může být podrobné zejména při častém provedení. MemoryOwner<T> má k tomu flexibilnější přístup prostřednictvím Allocate(int, AllocationMode) rozhraní API. Tato metoda nejen přiděluje novou instanci přesně požadované velikosti, ale lze také použít k určení režimu přidělení, který se má použít: buď stejný ArrayPool<T>jako , nebo ten, který automaticky vymaže pronajatou vyrovnávací paměť.
  • Existují případy, kdy se vyrovnávací paměť může pronajmout s větší velikostí, než je skutečně potřeba, a potom změnit velikost. Obvykle by to vyžadovalo, aby si uživatelé pronajali novou vyrovnávací paměť a zkopírovali oblast zájmu ze staré vyrovnávací paměti. Místo toho zveřejňuje Slice(int, int) rozhraní API, MemoryOwner<T> které jednoduše vrátí novou instanci, která zabalí zadanou oblast zájmu. To umožňuje přeskočit zapůjčení nové vyrovnávací paměti a úplné kopírování položek.

Syntaxe

Tady je příklad, jak si pronajmout vyrovnávací paměť a načíst Memory<T> instanci:

// 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;
}

V tomto příkladu using jsme použili blok k deklaraci MemoryOwner<T> vyrovnávací paměti: to je zvlášť užitečné, protože základní pole se automaticky vrátí do fondu na konci bloku. Pokud místo toho nemáme přímou kontrolu nad životností MemoryOwner<T> instance, vyrovnávací paměť se jednoduše vrátí do fondu, když je objekt finalizován uvolňováním paměti. V obou případech se zapůjčení vyrovnávacích pamětí vždy správně vrátí do sdíleného fondu.

Kdy se má použít?

MemoryOwner<T> lze použít jako typ vyrovnávací paměti pro obecné účely, který má výhodu minimalizace počtu přidělení provedených v průběhu času, protože interně opakovaně používá stejná pole ze sdíleného fondu. Běžným případem použití je nahrazení new T[] přidělení polí, zejména při provádění opakovaných operací, které vyžadují dočasnou vyrovnávací paměť pro práci nebo které v důsledku toho vytvářejí vyrovnávací paměť.

Předpokládejme, že máme datovou sadu skládající se z řady binárních souborů a že musíme číst všechny tyto soubory a zpracovávat je nějakým způsobem. Abychom mohli kód správně oddělit, můžeme nakonec zapsat metodu, která jednoduše čte jeden binární soubor, který může vypadat takto:

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;
}

Všimněte si výrazu new byte[] . Pokud si přečteme velký počet souborů, skončíme přidělením velkého množství nových polí, což bude mít velký tlak na uvolňování paměti. Tento kód bychom mohli chtít refaktorovat pomocí vyrovnávacích pamětí pronajmutých z fondu, například takto:

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);
}

Při použití tohoto přístupu se teď vyrovnávací paměti pronajímají z fondu, což znamená, že ve většině případů můžeme přidělení přeskočit. Kromě toho, protože pronajaté vyrovnávací paměti nejsou ve výchozím nastavení vymazány, můžeme také ušetřit čas potřebný k vyplnění nul, což nám dává další malé zlepšení výkonu. V předchozím příkladu by načtení 1 000 souborů přineslo celkovou velikost přidělení z přibližně 1 MB dolů na pouhých 1024 bajtů – efektivně by se přidělila jenom jedna vyrovnávací paměť a pak se znovu použila automaticky.

Výše uvedený kód má dva hlavní problémy:

  • ArrayPool<T> může vrátit vyrovnávací paměti, které mají větší velikost než požadovaná vyrovnávací paměť. Abychom tento problém vyřešili, musíme vrátit řazenou kolekci členů, která také označuje skutečnou použitou velikost do naší pronajaté vyrovnávací paměti.
  • Jednoduše vrácením pole musíme pečlivě sledovat jeho životnost a vrátit ho do příslušného fondu. Tento problém můžeme obejít tak, že místo toho použijeme MemoryPool<T> instanci a vrátíme IMemoryOwner<T> instanci, ale stále máme problém s pronajmutými vyrovnávacími pamětí větší než to, co potřebujeme. Kromě toho má určité režijní náklady při načítání Span<T> práce na práci, protože je to rozhraní, a skutečnost, IMemoryOwner<T> že vždy potřebujeme získat Memory<T> instanci nejprve a pak .Span<T>

K vyřešení obou těchto problémů můžeme tento kód znovu refaktorovat pomocí 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> Vrácená instance se postará o vyřazení základní vyrovnávací paměti a vrácení do fondu při vyvolání metodyIDisposable.Dispose. Můžeme ji použít k získání Memory<T> instance nebo Span<T> k interakci s načtenými daty a následnému odstranění instance, když ji už nepotřebujeme. Kromě toho všechny MemoryOwner<T> vlastnosti (například MemoryOwner<T>.Span) respektují počáteční požadovanou velikost, kterou jsme použili, takže už nemusíme ručně sledovat efektivní velikost v rámci pronajaté vyrovnávací paměti.

Příklady

Další příklady najdete v testech jednotek.