Speicher und Spanne Punkt 1

Ab .NET Core 2.0 und .NET Framework 4.5 können wir neue Datentypen verwenden: Span und Memory . Um sie zu verwenden, müssen Sie nur das System.Memory Nuget-Paket installieren:


PM> Install-Package System.Memory

Diese Datentypen sind bemerkenswert, da das CLR-Team hervorragende Arbeit geleistet hat, um ihre spezielle Unterstützung im Code des .NET Core 2.1+ JIT-Compilers zu implementieren, indem diese Datentypen direkt in den Core eingebettet wurden. Um welche Art von Datentypen handelt es sich und warum sind sie ein ganzes Kapitel wert?


Wenn wir über Probleme sprechen, die diese Typen auftreten ließen, sollte ich drei davon nennen. Der erste ist nicht verwalteter Code.


Sowohl die Sprache als auch die Plattform existieren seit vielen Jahren zusammen mit Mitteln zur Arbeit mit nicht verwaltetem Code. Warum also eine andere API für die Arbeit mit nicht verwaltetem Code freigeben, wenn die erstere im Grunde viele Jahre existiert? Um diese Frage zu beantworten, sollten wir verstehen, was uns vorher gefehlt hat.


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 .

Die Plattformentwickler haben bereits versucht, uns die Verwendung nicht verwalteter Ressourcen zu erleichtern. Sie implementierten Auto-Wrapper für importierte Methoden und Marshalling, die in den meisten Fällen automatisch funktionieren. Hier gehört auch zu stackalloc , das im Kapitel über einen Thread-Stack erwähnt wird. Aus meiner Sicht kamen die ersten C # -Entwickler aus der C ++ - Welt (mein Fall), aber jetzt wechseln sie von höheren Sprachen (ich kenne einen Entwickler, der zuvor in JavaScript geschrieben hat). Dies bedeutet, dass die Leute gegenüber nicht verwaltetem Code und C / C + -Konstrukten misstrauischer werden, umso mehr gegenüber Assembler.


Infolgedessen enthalten Projekte immer weniger unsicheren Code und das Vertrauen in die Plattform-API wächst immer mehr. Dies ist leicht zu überprüfen, ob wir in öffentlichen Repositorys nach stackalloc Anwendungsfällen suchen - sie sind rar. Nehmen wir jedoch jeden Code, der ihn verwendet:


Interop.ReadDir-Klasse
/src/mscorlib/shared/Interop/Unix/System.Native/Interop.ReadDir.cs


 unsafe { // s_readBufferSize is zero when the native implementation does not support reading into a buffer. byte* buffer = stackalloc byte[s_readBufferSize]; InternalDirectoryEntry temp; int ret = ReadDirR(dir.DangerousGetHandle(), buffer, s_readBufferSize, out temp); // We copy data into DirectoryEntry to ensure there are no dangling references. outputEntry = ret == 0 ? new DirectoryEntry() { InodeName = GetDirectoryEntryName(temp), InodeType = temp.InodeType } : default(DirectoryEntry); return ret; } 

Wir können sehen, warum es nicht beliebt ist. Überfliegen Sie einfach diesen Code und fragen Sie sich, ob Sie ihm vertrauen. Ich denke die Antwort ist "Nein". Dann fragen Sie sich warum. Es ist offensichtlich: Wir sehen nicht nur das Wort " Dangerous , was darauf hindeutet, dass etwas schief gehen könnte, sondern es gibt auch das unsafe Schlüsselwort und das byte* buffer = stackalloc byte[s_readBufferSize]; Zeile (speziell - byte* ), die unsere Einstellung ändert. Dies ist ein Auslöser für Sie zu denken: "Gab es nicht einen anderen Weg, dies zu tun"? Kommen wir also tiefer in die Psychoanalyse: Warum könnten Sie so denken? Einerseits verwenden wir Sprachkonstrukte und die hier angebotene Syntax ist weit entfernt von beispielsweise C ++ / CLI, die alles erlaubt (sogar das Einfügen von reinem Assembler-Code). Andererseits sieht diese Syntax ungewöhnlich aus.


Das zweite Problem, an das Entwickler implizit oder explizit gedacht haben, ist die Inkompatibilität von String- und Char [] -Typen. Logischerweise ist eine Zeichenfolge ein Array von Zeichen, aber Sie können keine Zeichenfolge in char [] umwandeln: Sie können nur ein neues Objekt erstellen und den Inhalt einer Zeichenfolge in ein Array kopieren. Diese Inkompatibilität wird eingeführt, um Zeichenfolgen hinsichtlich des Speichers zu optimieren (es gibt keine schreibgeschützten Arrays). Es treten jedoch Probleme auf, wenn Sie mit der Arbeit mit Dateien beginnen. Wie lese ich sie? Als String oder als Array? Wenn Sie ein Array auswählen, können Sie einige Methoden nicht verwenden, die für die Arbeit mit Zeichenfolgen entwickelt wurden. Was ist mit Lesen als Zeichenfolge? Es kann zu lang sein. Wenn Sie es dann analysieren müssen, welchen Parser sollten Sie für primitive Datentypen auswählen: Sie möchten sie nicht immer manuell analysieren (Ganzzahlen, Gleitkommazahlen, in verschiedenen Formaten angegeben). Wir haben viele bewährte Algorithmen, die dies schneller und effizienter machen, nicht wahr? Solche Algorithmen arbeiten jedoch häufig mit Zeichenfolgen, die nur einen primitiven Typ selbst enthalten. Es gibt also ein Dilemma.


Das dritte Problem besteht darin, dass die von einem Algorithmus benötigten Daten selten einen kontinuierlichen, soliden Datenschnitt innerhalb eines Abschnitts eines Arrays bilden, der aus einer Quelle gelesen wird. Zum Beispiel haben wir im Fall von Dateien oder Daten, die aus einem Socket gelesen wurden, einen Teil davon bereits von einem Algorithmus verarbeitet, gefolgt von einem Teil der Daten, die von unserer Methode verarbeitet werden müssen, und dann von noch nicht verarbeiteten Daten. Im Idealfall möchte unsere Methode nur die Daten, für die diese Methode entwickelt wurde. Beispielsweise ist eine Methode, die Ganzzahlen analysiert, mit einer Zeichenfolge, die einige Wörter mit einer erwarteten Zahl irgendwo darunter enthält, nicht zufrieden. Diese Methode will eine Nummer und sonst nichts. Wenn wir ein gesamtes Array übergeben, muss beispielsweise der Versatz für eine Zahl vom Anfang des Arrays angegeben werden.


 int ParseInt(char[] input, int index) { while(char.IsDigit(input[index])) { // ... index++; } } 

Dieser Ansatz ist jedoch schlecht, da diese Methode unnötige Daten erhält. Mit anderen Worten, die Methode wird für Kontexte aufgerufen, für die sie nicht entwickelt wurde , und muss einige externe Aufgaben lösen. Das ist ein schlechtes Design. Wie vermeide ich diese Probleme? Optional können wir den ArraySegment<T> -Typ verwenden, der Zugriff auf einen Abschnitt eines Arrays ArraySegment<T> :


 int ParseInt(IList<char>[] input) { while(char.IsDigit(input.Array[index])) { // ... index++; } } var arraySegment = new ArraySegment(array, from, length); var res = ParseInt((IList<char>)arraySegment); 

Ich denke jedoch, dass dies sowohl logisch als auch in Bezug auf die Leistung zu viel ist. ArraySegment ist schlecht gestaltet und verlangsamt den Zugriff auf Elemente im Vergleich zu denselben Operationen, die mit einem Array ausgeführt werden, um das 7-fache.


Wie lösen wir diese Probleme? Wie bringen wir Entwickler dazu, nicht verwalteten Code wieder zu verwenden, und geben ihnen ein einheitliches und schnelles Tool für die Arbeit mit heterogenen Datenquellen: Arrays, Zeichenfolgen und nicht verwalteten Speicher. Es war notwendig, ihnen das Gefühl des Vertrauens zu geben, dass sie nicht unwissentlich einen Fehler machen können. Es war notwendig, ihnen ein Instrument zu geben, das die nativen Datentypen nicht in Bezug auf die Leistung verringert, sondern die aufgeführten Probleme löst. Span<T> Typen Span<T> und Memory<T> sind genau diese Instrumente.


Span <T>, ReadOnlySpan <T>


Span Typ ist ein Instrument zum Arbeiten mit Daten innerhalb eines Abschnitts eines Datenarrays oder mit einem Teilbereich seiner Werte. Wie im Fall eines Arrays können sowohl die Elemente dieses Unterbereichs gelesen als auch geschrieben werden, jedoch mit einer wichtigen Einschränkung: Sie erhalten oder erstellen einen Span<T> nur für eine temporäre Arbeit mit einem Array. Rufen Sie einfach eine Gruppe von Methoden auf . Um ein allgemeines Verständnis zu erhalten, vergleichen wir jedoch die Datentypen, für die Span entwickelt wurde, und betrachten die möglichen Verwendungsszenarien.


Der erste Datentyp ist ein übliches Array. Arrays arbeiten mit Span folgendermaßen:


  var array = new [] {1,2,3,4,5,6}; var span = new Span<int>(array, 1, 3); var position = span.BinarySearch(3); Console.WriteLine(span[position]); // -> 3 

Zunächst erstellen wir ein Datenarray, wie in diesem Beispiel gezeigt. Als Nächstes erstellen wir Span (oder eine Teilmenge), die auf das Array verweist, und machen einen zuvor initialisierten Wertebereich für Code zugänglich, der das Array verwendet.


Hier sehen wir das erste Merkmal dieser Art von Daten, dh die Fähigkeit, einen bestimmten Kontext zu erstellen. Erweitern wir unsere Vorstellung von Kontexten:


 void Main() { var array = new [] {'1','2','3','4','5','6'}; var span = new Span<char>(array, 1, 3); if(TryParseInt32(span, out var res)) { Console.WriteLine(res); } else { Console.WriteLine("Failed to parse"); } } public bool TryParseInt32(Span<char> input, out int result) { result = 0; for (int i = 0; i < input.Length; i++) { if(input[i] < '0' || input[i] > '9') return false; result = result * 10 + ((int)input[i] - '0'); } return true; } ----- 234 

Wie wir sehen, bietet Span<T> abstrakten Zugriff auf einen Speicherbereich zum Lesen und Schreiben. Was gibt es uns? Wenn wir uns daran erinnern, wofür wir Span sonst noch verwenden können, werden wir über nicht verwaltete Ressourcen und Zeichenfolgen nachdenken:


 // Managed array var array = new[] { '1', '2', '3', '4', '5', '6' }; var arrSpan = new Span<char>(array, 1, 3); if (TryParseInt32(arrSpan, out var res1)) { Console.WriteLine(res1); } // String var srcString = "123456"; var strSpan = srcString.AsSpan(); if (TryParseInt32(strSpan, out var res2)) { Console.WriteLine(res2); } // void * Span<char> buf = stackalloc char[6]; buf[0] = '1'; buf[1] = '2'; buf[2] = '3'; buf[3] = '4'; buf[4] = '5'; buf[5] = '6'; if (TryParseInt32(buf, out var res3)) { Console.WriteLine(res3); } ----- 234 234 234 

Das bedeutet, dass Span<T> ein Tool ist, mit dem die Arbeitsweise mit verwaltetem und nicht verwaltetem Speicher vereinheitlicht werden kann. Es gewährleistet die Sicherheit beim Arbeiten mit solchen Daten während der Speicherbereinigung. Das heißt, wenn Speicherbereiche mit nicht verwalteten Ressourcen verschoben werden, ist dies sicher.


Sollten wir jedoch so aufgeregt sein? Könnten wir das früher erreichen? Bei verwalteten Arrays besteht beispielsweise kein Zweifel: Sie müssen nur ein Array in eine weitere Klasse einschließen (z. B. [ArraySegment] ( https://referencesource.microsoft.com/#mscorlib/system/). arraysegment.cs, 31 )) ergibt somit eine ähnliche Schnittstelle und das ist es. Darüber hinaus können Sie dasselbe mit Zeichenfolgen tun - sie verfügen über die erforderlichen Methoden. Auch hier müssen Sie nur eine Zeichenfolge in denselben Typ einschließen und Methoden bereitstellen, um damit zu arbeiten. Um jedoch einen String, einen Puffer und ein Array in einem Typ zu speichern, haben Sie viel damit zu tun, Verweise auf jede mögliche Variante in einer einzigen Instanz zu behalten (natürlich mit nur einer aktiven Variante).


 public readonly ref struct OurSpan<T> { private T[] _array; private string _str; private T * _buffer; // ... } 

Basierend auf der Architektur können Sie drei Typen erstellen, die eine einheitliche Schnittstelle implementieren. Daher ist es nicht möglich, eine einheitliche Schnittstelle zwischen diesen Datentypen zu erstellen, die sich von Span<T> und die maximale Leistung beizubehalten.


Als nächstes stellt sich die Frage, was ref struct in Bezug auf Span . Dies sind genau die „Strukturen, die nur auf dem Stapel existieren“, von denen wir bei Vorstellungsgesprächen so oft hören. Dies bedeutet, dass dieser Datentyp nur auf dem Stapel zugewiesen werden kann und nicht auf den Heap übertragen werden kann. Aus diesem Grund ist Span , eine Referenzstruktur, ein Kontextdatentyp, der die Arbeit von Methoden ermöglicht, nicht jedoch die von Objekten im Speicher. Darauf müssen wir uns stützen, wenn wir versuchen, es zu verstehen.


Jetzt können wir den Span Typ und den zugehörigen ReadOnlySpan Typ definieren:


Span ist ein Datentyp, der eine einheitliche Schnittstelle für die Arbeit mit heterogenen Arten von Datenarrays implementiert und die Übergabe einer Teilmenge eines Arrays an eine Methode ermöglicht, sodass die Zugriffsgeschwindigkeit auf das ursprüngliche Array unabhängig von der Tiefe des Arrays konstant und am höchsten ist Kontext.

In der Tat, wenn wir einen Code wie haben


 public void Method1(Span<byte> buffer) { buffer[0] = 0; Method2(buffer.Slice(1,2)); } Method2(Span<byte> buffer) { buffer[0] = 0; Method3(buffer.Slice(1,1)); } Method3(Span<byte> buffer) { buffer[0] = 0; } 

Die Geschwindigkeit des Zugriffs auf den ursprünglichen Puffer ist am höchsten, wenn Sie mit einem verwalteten Zeiger und nicht mit einem verwalteten Objekt arbeiten. Das bedeutet, dass Sie mit einem unsicheren Typ in einem verwalteten Wrapper arbeiten, jedoch nicht mit einem verwalteten .NET-Typ.


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 .

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


All Articles