[DotNetBook] Span, Memory und ReadOnlyMemory

Mit diesem Artikel veröffentliche ich weiterhin eine Reihe von Artikeln, deren Ergebnis ein Buch über die Arbeit von .NET CLR und .NET im Allgemeinen sein wird. Für Links - willkommen bei cat.


Speicher <T> und ReadOnlyMemory <T>


Es gibt zwei visuelle Unterschiede zwischen Memory<T> und Span<T> . Der erste ist, dass der Memory<T> keine ref Einschränkung im Typheader enthält. Das heißt, mit anderen Worten, der Typ Memory<T> hat das Recht, sich nicht nur auf dem Stapel zu befinden, entweder eine lokale Variable oder ein Parameter der Methode oder ihres Rückgabewerts, sondern auch auf dem Heap, der von dort auf einige Daten im Speicher verweist. Dieser kleine Unterschied macht jedoch einen großen Unterschied im Verhalten und in den Fähigkeiten von Memory<T> Vergleich zu Span<T> . Im Gegensatz zu Span<T> , bei dem für einige Methoden ein bestimmter Datenpuffer verwendet wird, dient der Memory<T> dazu, Informationen über den Puffer zu speichern und nicht damit zu arbeiten.


Hinweis


Das auf Habré veröffentlichte Kapitel ist nicht aktualisiert und wahrscheinlich bereits etwas veraltet. Wenden Sie sich daher für einen neueren Text dem Original zu:



Von hier kommt der Unterschied in der API:


  • Memory<T> enthält keine von ihm verwalteten Datenzugriffsmethoden. Stattdessen verfügt es über die Span Eigenschaft und die Slice Methode, die das Arbeitspferd zurückgeben - eine Instanz vom Typ Span .
  • Memory<T> enthält zusätzlich die Pin() -Methode, die für Skripte entwickelt wurde, wenn der gespeicherte Puffer an unsafe Code übergeben werden muss. Wenn es für Fälle aufgerufen wird, in denen der Speicher in .NET zugewiesen wurde, wird der Puffer fixiert und bewegt sich nicht, wenn der GC ausgelöst wird. MemoryHandle dem Benutzer eine Instanz der MemoryHandle Struktur zurückgegeben, die das Konzept einer GCHandle Lebensdauer kapselt, die den Puffer im Speicher festgelegt hat:

 public unsafe struct MemoryHandle : IDisposable { private void* _pointer; private GCHandle _handle; private IPinnable _pinnable; /// <summary> ///  MemoryHandle    /// </summary> public MemoryHandle(void* pointer, GCHandle handle = default, IPinnable pinnable = default) { _pointer = pointer; _handle = handle; _pinnable = pinnable; } /// <summary> ///     ,   ,       /// </summary> [CLSCompliant(false)] public void* Pointer => _pointer; /// <summary> ///  _handle  _pinnable,      /// </summary> public void Dispose() { if (_handle.IsAllocated) { _handle.Free(); } if (_pinnable != null) { _pinnable.Unpin(); _pinnable = null; } _pointer = null; } } 

Zunächst möchte ich jedoch die gesamte Klasse kennenlernen. Und als erstes werfen Sie einen Blick auf die Struktur des Memory<T> (nicht alle Typmitglieder werden angezeigt, aber diejenigen, die am wichtigsten zu sein scheinen):


  public readonly struct Memory<T> { private readonly object _object; private readonly int _index, _length; public Memory(T[] array) { ... } public Memory(T[] array, int start, int length) { ... } internal Memory(MemoryManager<T> manager, int length) { ... } internal Memory(MemoryManager<T> manager, int start, int length) { ... } public int Length => _length & RemoveFlagsBitMask; public bool IsEmpty => (_length & RemoveFlagsBitMask) == 0; public Memory<T> Slice(int start, int length); public void CopyTo(Memory<T> destination) => Span.CopyTo(destination.Span); public bool TryCopyTo(Memory<T> destination) => Span.TryCopyTo(destination.Span); } 

Neben der Angabe der Strukturfelder habe ich mich dazu entschlossen, zusätzlich darauf hinzuweisen, dass zwei weitere internal MemoryManager auf der Grundlage einer weiteren Entität arbeiten - des MemoryManager , auf den noch etwas näher eingegangen wird und den Sie möglicherweise nicht haben Gedanke: ein Speichermanager im klassischen Sinne. Wie Span enthält auch Memory einen Verweis auf das zu navigierende Objekt sowie den Versatz und die Größe des internen Puffers. Es ist auch erwähnenswert, dass Memory mit dem new Operator nur auf der Basis des Arrays plus Erweiterungsmethoden erstellt werden kann - auf der Basis der Zeichenfolge, des Arrays und des ArraySegment . Das heißt, Die manuelle Erstellung auf der Basis von nicht verwaltetem Speicher ist nicht impliziert. Wie wir sehen, gibt es jedoch einige interne Methoden zum Erstellen dieser Struktur basierend auf dem MemoryManager :


Datei MemoryManager.cs


 public abstract class MemoryManager<T> : IMemoryOwner<T>, IPinnable { public abstract MemoryHandle Pin(int elementIndex = 0); public abstract void Unpin(); public virtual Memory<T> Memory => new Memory<T>(this, GetSpan().Length); public abstract Span<T> GetSpan(); protected Memory<T> CreateMemory(int length) => new Memory<T>(this, length); protected Memory<T> CreateMemory(int start, int length) => new Memory<T>(this, start, length); void IDisposable.Dispose() protected abstract void Dispose(bool disposing); } 

Ich werde mir erlauben, ein wenig mit der im CLR-Befehl eingeführten Terminologie zu streiten und den Typ mit dem Namen MemoryManager zu benennen. Als ich ihn sah, entschied ich zuerst, dass es so etwas wie eine Speicherverwaltung sein würde, aber manuell, außer LOH / SOH. Aber er war sehr enttäuscht, die Realität zu sehen. Vielleicht sollten Sie es analog zur Schnittstelle aufrufen: MemoryOwner.

Was das Konzept des Besitzers eines Erinnerungsstücks zusammenfasst. Mit anderen Worten, wenn Span ein Mittel zum Arbeiten mit dem Speicher ist, ist Memory ein Mittel zum Speichern von Informationen über eine bestimmte Site, dann ist MemoryManager ein Mittel zum Steuern seines Lebens, seines Besitzers. Sie können beispielsweise den Typ NativeMemoryManager<T> , der, obwohl er für Tests geschrieben wurde, die Essenz des Konzepts des "Eigentums" nicht schlecht widerspiegelt:


NativeMemoryManager.cs- Datei


 internal sealed class NativeMemoryManager : MemoryManager<byte> { private readonly int _length; private IntPtr _ptr; private int _retainedCount; private bool _disposed; public NativeMemoryManager(int length) { _length = length; _ptr = Marshal.AllocHGlobal(length); } public override void Pin() { ... } public override void Unpin() { lock (this) { if (_retainedCount > 0) { _retainedCount--; if (_retainedCount == 0) { if (_disposed) { Marshal.FreeHGlobal(_ptr); _ptr = IntPtr.Zero; } } } } } //   } 

Das heißt, mit anderen Worten, die Klasse bietet die Möglichkeit verschachtelter Aufrufe der Pin() -Methode, wodurch die resultierenden Links aus der unsafe Welt gezählt werden.


Eine weitere Entität, die eng mit Memory verwandt ist, ist MemoryPool , das die Zusammenfassung von Instanzen von MemoryManager (und tatsächlich IMemoryOwner ) ermöglicht:


Datei MemoryPool.cs


 public abstract class MemoryPool<T> : IDisposable { public static MemoryPool<T> Shared => s_shared; public abstract IMemoryOwner<T> Rent(int minBufferSize = -1); public void Dispose() { ... } } 

Damit sollen Puffer der erforderlichen Größe für die vorübergehende Verwendung ausgegeben werden. Geleaste Instanzen, die die IMemoryOwner<T> -Schnittstelle implementieren, verfügen über eine Dispose() -Methode, die das geleaste Array an den Array-Pool zurückgibt. Standardmäßig können Sie den gemeinsam genutzten Pufferpool verwenden, der auf ArrayMemoryPool :


Datei ArrayMemoryPool.cs


 internal sealed partial class ArrayMemoryPool<T> : MemoryPool<T> { private const int MaximumBufferSize = int.MaxValue; public sealed override int MaxBufferSize => MaximumBufferSize; public sealed override IMemoryOwner<T> Rent(int minimumBufferSize = -1) { if (minimumBufferSize == -1) minimumBufferSize = 1 + (4095 / Unsafe.SizeOf<T>()); else if (((uint)minimumBufferSize) > MaximumBufferSize) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBufferSize); return new ArrayMemoryPoolBuffer(minimumBufferSize); } protected sealed override void Dispose(bool disposing) { } } 

Und auf der Grundlage dessen, was er sah, zeichnet sich das folgende Bild der Welt ab:


  • Der Span Datentyp muss in den Methodenparametern verwendet werden, wenn Sie entweder Daten lesen ( ReadOnlySpan ) oder schreiben ( Span ). Aber nicht die Aufgabe, es für die zukünftige Verwendung im Klassenfeld zu speichern
  • Wenn Sie einen Link zum Datenpuffer aus dem Klassenfeld speichern müssen, müssen Sie je nach Verwendungszweck Memory<T> oder ReadOnlyMemory<T> verwenden
  • MemoryManager<T> ist der Eigentümer des Datenpuffers (Sie können ihn nicht verwenden: falls erforderlich). Dies ist erforderlich, wenn beispielsweise Anrufe an Pin() . Oder wenn Sie wissen müssen, wie Sie Speicher freigeben können
  • Wenn der Memory um einen nicht verwalteten Speicherbereich herum aufgebaut ist, führt Pin() nichts aus. Dies vereinheitlicht jedoch die Arbeit mit verschiedenen Puffertypen: Sowohl bei verwaltetem als auch bei nicht verwaltetem Code ist die Interaktionsschnittstelle dieselbe
  • Jeder der Typen verfügt über öffentliche Konstruktoren. Dies bedeutet, dass Sie beide Span direkt verwenden und eine Kopie davon aus dem Memory . Sie können den Memory selbst entweder separat erstellen oder einen IMemoryOwner Typ dafür IMemoryOwner , der den Teil des Speichers besitzt, auf den der Memory verweist. Ein Sonderfall kann ein beliebiger Typ sein, der auf MemoryManager basiert: ein lokaler Besitz eines Speicherstücks (z. B. mit Referenzzählung aus einer unsafe Welt). Wenn Sie gleichzeitig solche Puffer ziehen müssen (erwarten Sie häufigen Verkehr von Puffern von ungefähr gleicher Größe), können Sie den MemoryPool Typ verwenden.
  • Wenn impliziert wird, dass Sie mit unsafe Code arbeiten und dort einen bestimmten Datenpuffer übergeben müssen, sollten Sie den Memory verwenden: Es verfügt über eine Pin Methode, die das Fixieren des Puffers im .NET-Heap automatisiert, sofern dort einer erstellt wurde.
  • Wenn Sie etwas Pufferverkehr haben (zum Beispiel lösen Sie das Problem des Parsens von Programmtext oder DSL), lohnt es sich, den MemoryPool Typ zu verwenden, der sehr korrekt organisiert werden kann und Puffer der entsprechenden Größe aus dem Pool ausgibt (z. B. etwas größer, wenn nicht geeignet) aber mit dem Beschneiden von originalMemory.Slice(requiredSize) , um den Pool nicht zu fragmentieren)

Link zum ganzen Buch



Source: https://habr.com/ru/post/de420051/


All Articles