
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- SpanEigenschaft und die- SliceMethode, die eine Instanz vom Typ- Span.
- Darüber hinaus enthält Memory<T>diePin()-Methode, die für Szenarien verwendet wird, in denen gespeicherte Pufferdaten anunsafeCode ü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 derMemoryHandleStruktur zurück, dieGCHandlekapselt, um ein Segment einer Lebensdauer anzugeben und den Array-Puffer im Speicher zuGCHandle.
 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.
 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 .
 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:
- SpanDatentyp 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>oderReadOnlyMemory<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 Memoryum einen nicht verwalteten Speicherbereich herum aufgebaut ist, kannPin()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 Spandirekt verwenden oder seine Instanz aus demMemory. Für denMemoryals solchen können Sie ihn einzeln erstellen oder einen Speicherbereich erstellen, derIMemoryOwnerund auf den derMemoryverweist. Jeder aufMemoryMangerbasierendeMemoryMangerkann als ein spezifischer Fall betrachtet werden, dem ein lokaler Speicherbereich gehört (z. B. begleitet vom Zählen der Referenzen aus derunsafeWelt). 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 denMemoryPoolTyp verwenden.
- Wenn Sie mit unsafeCode arbeitenunsafe, indem Sie dort einen Datenpuffer übergeben, sollten Sie denMemoryverwenden, der über diePin()-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 MemoryPoolTyp 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, aberoriginalMemory.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:
- ArraySegmentist schrecklich. Aber wenn Sie es in- Spaneinwickeln, 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 Spandie 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 Spaneinen Teil eines Datenpuffers verwenden kann, um den Kontext zu erstellen. Die gleiche Funktionalität ist inArraySegmentzu 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 Corehaben sie den einzigen Vorteil: Sie sind schneller alsArraySegmentwenn sie mit einer Teilmenge eines ursprünglichen Arrays arbeiten.
- In .NET Core 2.1+bietet ihre Verwendung einen unbestreitbaren Vorteil gegenüberArraySegmentund jeder manuellen Implementierung vonSlice.
- 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. 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.
 
 
