Speicher und Spanne Punkt 3


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).

Leistung


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.


Leistungsdiagramm


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.


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


All Articles