Freigeben über


MemoryOwner<T>

Der MemoryOwner<T> ist ein Puffertyp, der IMemoryOwner<T> implementiert, eine eingebettete Längeneigenschaft und eine Reihe leistungsorientierter APIs. Es ist im Wesentlichen ein einfacher Wrapper um den ArrayPool<T>-Typ, mit einigen zusätzlichen Hilfsprogrammen.

Plattform-APIs: MemoryOwner<T>, AllocationMode

Funktionsweise

MemoryOwner<T> hat die folgenden Hauptfunktionen:

  • Eines der Hauptprobleme von Arrays, die von den ArrayPool<T>-APIs zurückgegeben werden, und von IMemoryOwner<T>-Instanzen, die von den MemoryPool<T>-APIs zurückgegeben werden, besteht darin, dass die vom Benutzer angegebene Größe nur als Mindestgröße verwendet wird: Die tatsächliche Größe der zurückgegebenen Puffer kann größer sein. MemoryOwner<T> löst dies, indem auch die ursprünglich angeforderten Größe gespeichert wird, sodass Memory<T>- und Span<T>-Instanzen, die daraus abgerufen werden, niemals manuell segmentiert werden müssen.
  • Bei der Verwendung von IMemoryOwner<T> ist es erforderlich, zunächst eine Memory<T>-Instanz und dann eine Span<T> abzurufen, um eine Span<T> für den zugrunde liegenden Puffer zu erhalten. Dies ist ziemlich teuer und oft unnötig, da der Zwischenschritt Memory<T> vielleicht gar nicht benötigt wird. MemoryOwner<T> verfügt stattdessen über eine zusätzliche Span-Eigenschaft, die extrem einfach ist, da sie direkt das interne T[]-Array umschließt, das vom Pool gemietet wird.
  • Puffer, die vom Pool gemietet werden, werden nicht standardmäßig gelöscht, was bedeutet, dass wenn sie bei der vorherigen Rückgabe an den Pool nicht gelöscht wurden, könnten sie Datenmüll enthalten. Normalerweise müssen Benutzer diese gemieteten Puffer manuell löschen, was besonders bei häufigen Aufgaben aufwendig sein kann. MemoryOwner<T> hat einen flexibleren Ansatz dafür über die Allocate(int, AllocationMode)-API. Diese Methode teilt nicht nur eine neue Instanz genau der angeforderten Größe zu, sondern kann auch verwendet werden, um anzugeben, welcher Zuordnungsmodus verwendet werden soll: entweder denselben wie ArrayPool<T>, oder eine Instanz, die den gemieteten Puffer automatisch löscht.
  • Es gibt Fälle, in denen ein Puffer möglicherweise mit einer größeren Größe gemietet wird als tatsächlich benötigt, und die Größe wird dann nachträglich geändert. Dies würde in der Regel erfordern, dass Benutzer einen neuen Puffer mieten und die betreffende Region aus dem alten Puffer kopieren. MemoryOwner<T> stellt stattdessen eine Slice(int, int)-API zur Verfügung, die einfach eine neue Instanz zurückgibt, die den angegebenen Interessenbereich umschließt. Dies ermöglicht es, die Miete eines neuen Puffers zu überspringen und die Elemente vollständig zu kopieren.

Syntax

Hier ist ein Beispiel für das Mieten eines Puffers und das Abrufen einer Memory<T>-Instanz:

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

In diesem Beispiel haben wir einen using-Block zum Deklarieren des MemoryOwner<T>-Puffers verwendet: Dies ist besonders nützlich, da das zugrunde liegende Array am Ende des Blocks automatisch an den Pool zurückgegeben wird. Wenn wir stattdessen keine direkte Kontrolle über die Lebensdauer einer MemoryOwner<T>-Instanz haben, wird der Puffer einfach an den Pool zurückgegeben, wenn das Objekt vom Garbage Collector abgeschlossen wird. In beiden Fällen werden gemietete Puffer immer ordnungsgemäß an den freigegebenen Pool zurückgegeben.

Wann sollte dies verwendet werden?

MemoryOwner<T> kann als universeller Puffertyp verwendet werden, was den Vorteil hat, dass die Anzahl der im Laufe der Zeit durchgeführten Zuweisungen minimiert wird, da intern dieselben Arrays aus einem freigegebenen Pool wiederverwendet werden. Ein gängiger Anwendungsfall besteht darin, new T[]-Arrayzuteilungen zu ersetzen, insbesondere wenn wiederholte Vorgänge ausgeführt werden, für die entweder ein temporärer Puffer erforderlich ist oder ein Puffer als Ergebnis erzeugt wird.

Angenommen, wir haben ein Dataset, das aus einer Reihe von Binärdateien besteht, und wir müssen alle diese Dateien lesen und auf irgendeine Weise verarbeiten. Um unseren Code ordnungsgemäß zu trennen, schreiben wir möglicherweise eine Methode, die einfach eine Binärdatei liest, was wie folgt aussehen könnte:

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

Beachten Sie diesen new byte[]-Ausdruck. Wenn wir eine große Anzahl von Dateien lesen, werden wir viele neue Arrays zuweisen, was den Garbage Collector stark beanspruchen wird. Wir könnten diesen Code mithilfe von Puffern, die aus einem Pool gemietet werden, wie folgt umgestalten:

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

Bei Verwendung dieses Ansatzes werden Puffer jetzt aus einem Pool gemietet, was bedeutet, dass wir in den meisten Fällen eine Zuteilung überspringen können. Da gemietete Puffer standardmäßig nicht gelöscht werden, können wir auch die Zeit sparen, um sie mit Nullen zu füllen, was uns eine weitere kleine Leistungsverbesserung ermöglicht. Im obigen Beispiel würde das Laden von 1000 Dateien die Gesamtzuteilungsgröße von etwa 1 MB auf nur 1024 Byte herunterbringen – nur ein einzelner Puffer würde effektiv zugeteilt und dann automatisch wiederverwendet.

Es gibt zwei Hauptprobleme mit dem obigen Code:

  • ArrayPool<T> könnte Puffer zurückgeben, die größer als die angeforderte Größe sind. Um dieses Problem zu umgehen, müssen wir ein Tupel zurückgeben, das auch die tatsächlich verwendete Größe in unseren gemieteten Puffer angibt.
  • Wenn wir einfach ein Array zurückgeben, müssen wir besonders darauf achten, dass wir die Lebensdauer des Arrays genau verfolgen und es an den entsprechenden Pool zurückgeben. Wir können dieses Problem möglicherweise umgehen, indem wir stattdessen MemoryPool<T> verwenden und eine IMemoryOwner<T>-Instanz zurückgeben, aber wir haben immer noch das Problem, dass gemietete Puffer eine größere Größe haben als das, was wir benötigen. Darüber hinaus verursacht IMemoryOwner<T> einen gewissen Mehraufwand beim Abrufen einer Span<T>, um daran zu arbeiten, da es sich um eine Schnittstelle handelt und wir immer zuerst eine Memory<T>-Instanz und dann eine Span<T> abrufen müssen.

Um beide Probleme zu lösen, können wir diesen Code erneut umgestalten, indem wir MemoryOwner<T> verwenden:

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

Die zurückgegebene IMemoryOwner<byte>-Instanz wird sich um das Löschen des zugrunde liegenden Puffers kümmern und ihn an den Pool zurückgeben, wenn ihre IDisposable.Dispose-Methode aufgerufen wird. Wir können sie verwenden, um eine Memory<T>- oder Span<T>-Instanz abzurufen, um mit den geladenen Daten zu interagieren, und dann die Instanz löschen, wenn wir sie nicht mehr benötigen. Darüber hinaus respektieren alle MemoryOwner<T>-Eigenschaften (wie MemoryOwner<T>.Span) die ursprüngliche angeforderte Größe, die wir verwendet haben, so dass wir nicht mehr manuell die effektive Größe innerhalb des gemieteten Puffers nachverfolgen müssen.

Beispiele

Weitere Beispiele finden Sie in den Komponententests.