
Speicher <T> und ReadOnlyMemory <T>
Es gibt zwei visuelle Unterschiede zwischen Memory<T>
und Span<T>
. Der erste ist, dass der Memory<T>
-Typ keinen ref
Modifikator im Header des Typs enthält. Mit anderen Worten, der Memory<T>
kann sowohl auf dem Stapel als auch als lokale Variable oder als Methodenparameter oder als Rückgabewert als auch auf dem Heap zugewiesen werden, wobei von dort aus auf einige Daten im Speicher verwiesen wird. Dieser kleine Unterschied führt jedoch zu einem großen Unterschied im Verhalten und den Fähigkeiten von Memory<T>
Vergleich zu Span<T>
. Im Gegensatz zu Span<T>
, einem Instrument für einige Methoden zur Verwendung eines Datenpuffers, dient der Typ Memory<T>
zum Speichern von Informationen über den Puffer, jedoch nicht zum Behandeln. Somit gibt es den Unterschied in der API.
Memory<T>
verfügt nicht über Methoden, um auf die Daten zuzugreifen, für die er verantwortlich ist. Stattdessen verfügt es über die Span
Eigenschaft und die Slice
Methode, die eine Instanz vom Typ Span
.- Darüber hinaus enthält
Memory<T>
die Pin()
-Methode, die für Szenarien verwendet wird, in denen gespeicherte Pufferdaten an unsafe
Code übergeben werden sollen. Wenn diese Methode aufgerufen wird, wenn Speicher in .NET zugewiesen wird, wird der Puffer fixiert und nicht verschoben, wenn GC aktiv ist. Diese Methode gibt eine Instanz der MemoryHandle
Struktur zurück, die GCHandle
kapselt, um ein Segment einer Lebensdauer anzugeben und den Array-Puffer im Speicher zu GCHandle
.
Dieses Kapitel wurde vom Autor und von professionellen Übersetzern gemeinsam aus dem Russischen übersetzt . Sie können uns bei der Übersetzung von Russisch oder Englisch in eine andere Sprache helfen, hauptsächlich ins Chinesische oder Deutsche.
Wenn Sie sich bei uns bedanken möchten, können Sie dies am besten tun, indem Sie uns einen Stern auf Github geben oder das Repository teilen
github / sidristij / dotnetbook .
Ich schlage jedoch vor, dass wir uns mit der gesamten Klasse vertraut machen. Schauen wir uns zunächst die Memory<T>
-Struktur selbst an (hier zeige ich nur die Typmitglieder, die ich am wichtigsten fand):
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); public Span<T> Span { get; } public unsafe MemoryHandle Pin(); }
Wie wir sehen, enthält die Struktur den Konstruktor basierend auf Arrays, speichert jedoch Daten im Objekt. Hiermit werden zusätzlich auf Zeichenfolgen verwiesen, für die kein Konstruktor entwickelt wurde, die jedoch mit der AsMemory()
verwendet werden können. Es wird AsMemory()
. Da beide Typen jedoch binär ähnlich sein sollten, ist Object
der Typ des _object
.
Als nächstes sehen wir zwei Konstruktoren, die auf MemoryManager
basieren. Wir werden später darüber sprechen. Die Eigenschaften zum IsEmpty
von Length
(Größe) und IsEmpty
prüfen auf einen leeren Satz. Es gibt auch die Slice
Methode zum CopyTo
einer Teilmenge sowie die CopyTo
und TryCopyTo
Kopiermethoden.
Apropos Memory
Ich möchte zwei Methoden dieses Typs im Detail beschreiben: die Span
Eigenschaft und die Pin
Methode.
Speicher <T> .Span
public Span<T> Span { get { if (_index < 0) { return ((MemoryManager<T>)_object).GetSpan().Slice(_index & RemoveFlagsBitMask, _length); } else if (typeof(T) == typeof(char) && _object is string s) { // This is dangerous, returning a writable span for a string that should be immutable. // However, we need to handle the case where a ReadOnlyMemory<char> was created from a string // and then cast to a Memory<T>. Such a cast can only be done with unsafe or marshaling code, // in which case that's the dangerous operation performed by the dev, and we're just following // suit here to make it work as best as possible. return new Span<T>(ref Unsafe.As<char, T>(ref s.GetRawStringData()), s.Length).Slice(_index, _length); } else if (_object != null) { return new Span<T>((T[])_object, _index, _length & RemoveFlagsBitMask); } else { return default; } } }
Nämlich die Zeilen, die die Zeichenfolgenverwaltung übernehmen. Sie sagen, wenn wir ReadOnlyMemory<T>
in Memory<T>
konvertieren (diese Dinge sind in der binären Darstellung gleich und es gibt sogar einen Kommentar, müssen diese Typen auf binäre Weise zusammenfallen, da einer durch den Aufruf von Unsafe.As
aus einem anderen Unsafe.As
). Wir erhalten Zugang zu einer geheimen Kammer mit der Möglichkeit, die Saiten zu wechseln. Dies ist ein äußerst gefährlicher Mechanismus:
unsafe void Main() { var str = "Hello!"; ReadOnlyMemory<char> ronly = str.AsMemory(); Memory<char> mem = (Memory<char>)Unsafe.As<ReadOnlyMemory<char>, Memory<char>>(ref ronly); mem.Span[5] = '?'; Console.WriteLine(str); } --- Hello?
Dieser Mechanismus in Kombination mit der Internierung von Zeichenfolgen kann schwerwiegende Folgen haben.
Speicher <T> .Pin
Die zweite Methode, die große Aufmerksamkeit auf sich zieht, ist Pin
:
public unsafe MemoryHandle Pin() { if (_index < 0) { return ((MemoryManager<T>)_object).Pin((_index & RemoveFlagsBitMask)); } else if (typeof(T) == typeof(char) && _object is string s) { // This case can only happen if a ReadOnlyMemory<char> was created around a string // and then that was cast to a Memory<char> using unsafe / marshaling code. This needs // to work, however, so that code that uses a single Memory<char> field to store either // a readable ReadOnlyMemory<char> or a writable Memory<char> can still be pinned and // used for interop purposes. GCHandle handle = GCHandle.Alloc(s, GCHandleType.Pinned); void* pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref s.GetRawStringData()), _index); return new MemoryHandle(pointer, handle); } else if (_object is T[] array) { // Array is already pre-pinned if (_length < 0) { void* pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref array.GetRawSzArrayData()), _index); return new MemoryHandle(pointer); } else { GCHandle handle = GCHandle.Alloc(array, GCHandleType.Pinned); void* pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref array.GetRawSzArrayData()), _index); return new MemoryHandle(pointer, handle); } } return default; }
Es ist auch ein wichtiges Instrument für die Vereinheitlichung, denn wenn wir einen Puffer an nicht verwalteten Code übergeben möchten, müssen wir nur die Pin()
-Methode aufrufen und einen Zeiger auf diesen Code übergeben, unabhängig davon, auf welche Art von Daten sich Memory<T>
bezieht. Dieser Zeiger wird in der Eigenschaft einer resultierenden Struktur gespeichert.
void PinSample(Memory<byte> memory) { using(var handle = memory.Pin()) { WinApi.SomeApiMethod(handle.Pointer); } }
Es spielt keine Rolle, wofür Pin()
in diesem Code aufgerufen wurde: Es kann sich um Memory
, der entweder T[]
, oder um eine string
oder einen Puffer nicht verwalteten Speichers. Nur Arrays und Strings erhalten ein echtes GCHandle.Alloc(array, GCHandleType.Pinned)
und im Falle eines nicht verwalteten Speichers passiert nichts.
MemoryManager, IMemoryOwner, MemoryPool
Neben der Angabe von Strukturfeldern möchte ich darauf hinweisen, dass es zwei weitere internal
MemoryManager
gibt, die auf einer anderen Entität basieren - MemoryManager
. Dies ist kein klassischer Speichermanager, an den Sie vielleicht gedacht haben, und wir werden später darüber sprechen. klassischer Speichermanager, an den Sie vielleicht gedacht haben, und wir werden später darüber sprechen. Wie Span
hat Memory
einen Verweis auf ein navigiertes Objekt, einen Offset und die Größe eines internen Puffers. Beachten Sie, dass Sie den new
Operator verwenden können, um Memory
aus einem Array zu erstellen. Sie können auch Erweiterungsmethoden verwenden, um Memory
aus einer Zeichenfolge, einem Array oder einem ArraySegment
zu erstellen. Ich meine, es ist nicht dafür ausgelegt, manuell aus nicht verwaltetem Speicher erstellt zu werden. Wir können jedoch sehen, dass es eine interne Methode gibt, um diese Struktur mit MemoryManager
zu erstellen.
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); }
Diese Struktur gibt den Eigentümer eines Speicherbereichs an. Mit anderen Worten, Span
ist ein Instrument zum Arbeiten mit dem Speicher, Memory
ist ein Tool zum Speichern der Informationen über einen bestimmten Speicherbereich und MemoryManager
ist ein Tool zum Steuern der Lebensdauer dieses Bereichs, d. H. Des Besitzers. Zum Beispiel können wir uns den Typ NativeMemoryManager<T>
ansehen. Obwohl es für Tests verwendet wird, repräsentiert dieser Typ eindeutig das Konzept des „Eigentums“.
Datei NativeMemoryManager.cs
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; } } } } } // Other methods }
Das bedeutet, dass die Klasse verschachtelte Aufrufe der Pin()
-Methode zulässt und somit generierte Referenzen aus der unsafe
Welt zählt.
Eine andere Entität, die eng mit Memory
ist, ist MemoryPool
, das MemoryManager
Instanzen zusammenfasst ( IMemoryOwner
in der Tat):
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() { ... } }
Es wird verwendet, um Puffer einer für die vorübergehende Verwendung erforderlichen Größe zu leasen. Die geleasten Instanzen mit der implementierten IMemoryOwner<T>
-Schnittstelle verfügen über die Dispose()
-Methode, um das geleaste Array an den Array-Pool zurückzugeben. Standardmäßig können Sie den gemeinsam nutzbaren 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) { } }
Basierend auf dieser Architektur haben wir das folgende Bild:
Span
Datentyp sollte als Methodenparameter verwendet werden, wenn Sie Daten lesen ( ReadOnlySpan
) oder Daten lesen und schreiben ( Span
) möchten. Es sollte jedoch nicht für die zukünftige Verwendung in einem Feld einer Klasse gespeichert werden.- Wenn Sie eine Referenz aus einem Feld einer Klasse in einem Datenpuffer speichern müssen, müssen Sie je nach Ihren Zielen
Memory<T>
oder ReadOnlyMemory<T>
. MemoryManager<T>
ist der Eigentümer eines Datenpuffers (optional). Dies kann erforderlich sein, wenn Sie beispielsweise Pin()
-Aufrufe zählen müssen. Oder wenn Sie wissen müssen, wie Speicher freigegeben wird.- Wenn der
Memory
um einen nicht verwalteten Speicherbereich herum aufgebaut ist, kann Pin()
nichts tun. Diese Uniformen arbeiten jedoch mit verschiedenen Puffertypen: Sowohl für verwalteten als auch für nicht verwalteten Code ist die Interaktionsschnittstelle identisch. - Jeder Typ hat öffentliche Konstruktoren. Das heißt, Sie können
Span
direkt verwenden oder seine Instanz aus dem Memory
. Für den Memory
als solchen können Sie ihn einzeln erstellen oder einen Speicherbereich erstellen, der IMemoryOwner
und auf den der Memory
verweist. Jeder auf MemoryManger
basierende MemoryManger
kann als ein spezifischer Fall betrachtet werden, dem ein lokaler Speicherbereich gehört (z. B. begleitet vom Zählen der Referenzen aus der unsafe
Welt). Wenn Sie solche Puffer bündeln müssen (der häufige Verkehr von fast gleich großen Puffern wird erwartet), können Sie außerdem den MemoryPool
Typ verwenden. - Wenn Sie mit
unsafe
Code arbeiten unsafe
, indem Sie dort einen Datenpuffer übergeben, sollten Sie den Memory
verwenden, der über die Pin()
-Methode verfügt, mit der ein Puffer automatisch auf dem .NET-Heap fixiert wird, wenn er dort erstellt wurde. - Wenn Sie etwas Pufferverkehr haben (z. B. wenn Sie einen Programmtext oder DSL analysieren), ist es besser, den
MemoryPool
Typ zu verwenden. Sie können es ordnungsgemäß implementieren, um die Puffer einer erforderlichen Größe aus einem Pool auszugeben (z. B. einen etwas größeren Puffer, wenn kein geeigneter vorhanden ist, aber originalMemory.Slice(requiredSize)
, um eine Poolfragmentierung zu vermeiden).
Um die Leistung neuer Datentypen zu messen, habe ich mich für eine Bibliothek entschieden, die bereits zum Standard von BenchmarkDotNet geworden ist :
[Config(typeof(MultipleRuntimesConfig))] public class SpanIndexer { private const int Count = 100; private char[] arrayField; private ArraySegment<char> segment; private string str; [GlobalSetup] public void Setup() { str = new string(Enumerable.Repeat('a', Count).ToArray()); arrayField = str.ToArray(); segment = new ArraySegment<char>(arrayField); } [Benchmark(Baseline = true, OperationsPerInvoke = Count)] public int ArrayIndexer_Get() { var tmp = 0; for (int index = 0, len = arrayField.Length; index < len; index++) { tmp = arrayField[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public void ArrayIndexer_Set() { for (int index = 0, len = arrayField.Length; index < len; index++) { arrayField[index] = '0'; } } [Benchmark(OperationsPerInvoke = Count)] public int ArraySegmentIndexer_Get() { var tmp = 0; var accessor = (IList<char>)segment; for (int index = 0, len = accessor.Count; index < len; index++) { tmp = accessor[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public void ArraySegmentIndexer_Set() { var accessor = (IList<char>)segment; for (int index = 0, len = accessor.Count; index < len; index++) { accessor[index] = '0'; } } [Benchmark(OperationsPerInvoke = Count)] public int StringIndexer_Get() { var tmp = 0; for (int index = 0, len = str.Length; index < len; index++) { tmp = str[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public int SpanArrayIndexer_Get() { var span = arrayField.AsSpan(); var tmp = 0; for (int index = 0, len = span.Length; index < len; index++) { tmp = span[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public int SpanArraySegmentIndexer_Get() { var span = segment.AsSpan(); var tmp = 0; for (int index = 0, len = span.Length; index < len; index++) { tmp = span[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public int SpanStringIndexer_Get() { var span = str.AsSpan(); var tmp = 0; for (int index = 0, len = span.Length; index < len; index++) { tmp = span[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public void SpanArrayIndexer_Set() { var span = arrayField.AsSpan(); for (int index = 0, len = span.Length; index < len; index++) { span[index] = '0'; } } [Benchmark(OperationsPerInvoke = Count)] public void SpanArraySegmentIndexer_Set() { var span = segment.AsSpan(); for (int index = 0, len = span.Length; index < len; index++) { span[index] = '0'; } } } public class MultipleRuntimesConfig : ManualConfig { public MultipleRuntimesConfig() { Add(Job.Default .With(CsProjClassicNetToolchain.Net471) // Span not supported by CLR .WithId(".NET 4.7.1")); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp20) // Span supported by CLR .WithId(".NET Core 2.0")); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp21) // Span supported by CLR .WithId(".NET Core 2.1")); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp22) // Span supported by CLR .WithId(".NET Core 2.2")); } }
Lassen Sie uns nun die Ergebnisse sehen.

Wenn wir sie uns ansehen, können wir folgende Informationen erhalten:
ArraySegment
ist schrecklich. Aber wenn Sie es in Span
einwickeln, können Sie es weniger schrecklich machen. In diesem Fall erhöht sich die Leistung um das 7-fache.- Wenn wir .NET Framework 4.7.1 in Betracht ziehen (dasselbe gilt für 4.5), verringert die Verwendung von
Span
die Leistung beim Arbeiten mit Datenpuffern erheblich. Sie wird um 30–35% abnehmen. - Wenn wir uns jedoch .NET Core 2.1+ ansehen, bleibt die Leistung ähnlich oder steigt sogar, da
Span
einen Teil eines Datenpuffers verwenden kann, um den Kontext zu erstellen. Die gleiche Funktionalität ist in ArraySegment
zu finden, funktioniert jedoch sehr langsam.
Somit können wir einfache Schlussfolgerungen hinsichtlich der Verwendung dieser Datentypen ziehen:
- Für
.NET Framework 4.5+
und .NET Core
haben sie den einzigen Vorteil: Sie sind schneller als ArraySegment
wenn sie mit einer Teilmenge eines ursprünglichen Arrays arbeiten. - In
.NET Core 2.1+
bietet ihre Verwendung einen unbestreitbaren Vorteil gegenüber ArraySegment
und jeder manuellen Implementierung von Slice
. - Alle drei Möglichkeiten sind so produktiv wie möglich und dies kann mit keinem Tool zur Vereinheitlichung von Arrays erreicht werden.
Dieses Kapitel wurde vom Autor und von professionellen Übersetzern gemeinsam aus dem Russischen übersetzt . Sie können uns bei der Übersetzung von Russisch oder Englisch in eine andere Sprache helfen, hauptsächlich ins Chinesische oder Deutsche.
