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
