
Anwendungsbeispiele für Span <T>
Ein Mensch kann von Natur aus den Zweck eines bestimmten Instruments nicht vollständig verstehen, bis er oder sie Erfahrung gesammelt hat. Wenden wir uns also einigen Beispielen zu.
ValueStringBuilder
Eines der interessantesten Beispiele in Bezug auf Algorithmen ist der ValueStringBuilder
Typ. Es ist jedoch tief in mscorlib vergraben und mit dem internal
Modifikator als viele andere sehr interessante Datentypen gekennzeichnet. Dies bedeutet, dass wir dieses bemerkenswerte Instrument zur Optimierung nicht finden würden, wenn wir den mscorlib-Quellcode nicht untersucht hätten.
Was ist der Hauptnachteil des StringBuilder
? Sein Hauptnachteil ist der Typ und seine Basis - es ist ein Referenztyp und basiert auf char[]
, dh einem Zeichenarray. Zumindest bedeutet dies zwei Dinge: Wir verwenden den Heap (wenn auch nicht viel) und erhöhen die Wahrscheinlichkeit, das CPU-Geld zu verpassen.
Ein weiteres Problem mit StringBuilder
, mit dem ich konfrontiert war, ist die Konstruktion kleiner Zeichenfolgen. In diesem Fall muss die resultierende Zeichenfolge kurz sein, z. B. weniger als 100 Zeichen. Eine kurze Formatierung wirft Leistungsprobleme auf.
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 .
$"{x} is in range [{min};{max}]"
Inwieweit ist diese Variante schlechter als die manuelle Konstruktion durch StringBuilder
? Die Antwort ist nicht immer offensichtlich. Dies hängt vom Bauort und der Häufigkeit des Aufrufs dieser Methode ab. Zunächst weist string.Format
Speicher für den internen StringBuilder
, der ein Array von Zeichen erstellt (SourceString.Length + args.Length * 8). Wenn sich während der Array-Erstellung herausstellt, dass die Länge falsch bestimmt wurde, wird ein weiterer StringBuilder
erstellt, um den Rest zu erstellen. Dies führt zur Erstellung einer einzelnen verknüpften Liste. Infolgedessen muss die konstruierte Zeichenfolge zurückgegeben werden, was ein weiteres Kopieren bedeutet. Das ist eine Verschwendung. Es wäre großartig, wenn wir das Array eines gebildeten Strings auf dem Heap nicht mehr zuordnen könnten: Dies würde eines unserer Probleme lösen.
Schauen wir uns diesen Typ aus der Tiefe von mscorlib
:
ValueStringBuilder-Klasse
/ src / mscorlib / shared / System / Text / ValueStringBuilder
internal ref struct ValueStringBuilder { // this field will be active if we have too many characters private char[] _arrayToReturnToPool; // this field will be the main private Span<char> _chars; private int _pos; // the type accepts the buffer from the outside, delegating the choice of its size to a calling party public ValueStringBuilder(Span<char> initialBuffer) { _arrayToReturnToPool = null; _chars = initialBuffer; _pos = 0; } public int Length { get => _pos; set { int delta = value - _pos; if (delta > 0) { Append('\0', delta); } else { _pos = value; } } } // Here we get the string by copying characters from the array into another array public override string ToString() { var s = new string(_chars.Slice(0, _pos)); Clear(); return s; } // To insert a required character into the middle of the string //you should add space into the characters of that string and then copy that character public void Insert(int index, char value, int count) { if (_pos > _chars.Length - count) { Grow(count); } int remaining = _pos - index; _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); _chars.Slice(index, count).Fill(value); _pos += count; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char c) { int pos = _pos; if (pos < _chars.Length) { _chars[pos] = c; _pos = pos + 1; } else { GrowAndAppend(c); } } [MethodImpl(MethodImplOptions.NoInlining)] private void GrowAndAppend(char c) { Grow(1); Append(c); } // If the original array passed by the constructor wasn't enough // we allocate an array of a necessary size from the pool of free arrays // It would be ideal if the algorithm considered // discreteness of array size to avoid pool fragmentation. [MethodImpl(MethodImplOptions.NoInlining)] private void Grow(int requiredAdditionalCapacity) { Debug.Assert(requiredAdditionalCapacity > _chars.Length - _pos); char[] poolArray = ArrayPool<char>.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2)); _chars.CopyTo(poolArray); char[] toReturn = _arrayToReturnToPool; _chars = _arrayToReturnToPool = poolArray; if (toReturn != null) { ArrayPool<char>.Shared.Return(toReturn); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Clear() { char[] toReturn = _arrayToReturnToPool; this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again if (toReturn != null) { ArrayPool<char>.Shared.Return(toReturn); } } // Missing methods: the situation is crystal clear private void AppendSlow(string s); public bool TryCopyTo(Span<char> destination, out int charsWritten); public void Append(string s); public void Append(char c, int count); public unsafe void Append(char* value, int length); public Span<char> AppendSpan(int length); }
Diese Klasse ähnelt funktional ihrem älteren StringBuilder
Kollegen, hat jedoch eine interessante und sehr wichtige Funktion: Es handelt sich um einen Wertetyp. Das heißt, es wird gespeichert und vollständig als Wert übergeben. Ein neuer Modifikator für den ref
Typ, der Teil einer Typdeklarationssignatur ist, weist darauf hin, dass dieser Typ eine zusätzliche Einschränkung aufweist: Er kann nur auf dem Stapel zugewiesen werden. Ich meine, das Übergeben seiner Instanzen an Klassenfelder führt zu einem Fehler. Wofür ist das alles? Um diese Frage zu beantworten, müssen Sie sich nur die StringBuilder
Klasse ansehen, deren Kern wir gerade beschrieben haben:
StringBuilder-Klasse /src/mscorlib/src/System/Text/StringBuilder.cs
public sealed class StringBuilder : ISerializable { // A StringBuilder is internally represented as a linked list of blocks each of which holds // a chunk of the string. It turns out string as a whole can also be represented as just a chunk, // so that is what we do. internal char[] m_ChunkChars; // The characters in this block internal StringBuilder m_ChunkPrevious; // Link to the block logically before this block internal int m_ChunkLength; // The index in m_ChunkChars that represent the end of the block internal int m_ChunkOffset; // The logical offset (sum of all characters in previous blocks) internal int m_MaxCapacity = 0; // ... internal const int DefaultCapacity = 16;
StringBuilder
ist eine Klasse, die einen Verweis auf ein Array von Zeichen enthält. Wenn Sie es erstellen, werden tatsächlich zwei Objekte StringBuilder
: StringBuilder
und ein Array von Zeichen mit einer Größe von mindestens 16 Zeichen. Aus diesem Grund ist es wichtig, die erwartete Länge einer Zeichenfolge festzulegen: Sie wird erstellt, indem eine einzelne verknüpfte Liste von Arrays mit jeweils 16 Zeichen generiert wird. Gib zu, das ist eine Verschwendung. In Bezug auf den ValueStringBuilder
Typ bedeutet dies keine Standardkapazität, da externer Speicher ausgeliehen wird. Außerdem handelt es sich um einen Werttyp, bei dem ein Benutzer einen Puffer für Zeichen auf dem Stapel zuweist. Somit wird die gesamte Instanz eines Typs zusammen mit seinem Inhalt auf den Stapel gelegt und das Problem der Optimierung gelöst. Da auf dem Heap kein Speicher zugewiesen werden muss, gibt es keine Probleme mit einer Leistungsminderung beim Umgang mit dem Heap. Vielleicht haben Sie eine Frage: Warum verwenden wir nicht immer ValueStringBuilder
(oder das benutzerdefinierte Analog, da wir das Original nicht verwenden können, weil es intern ist)? Die Antwort lautet: Es kommt auf eine Aufgabe an. Wird eine resultierende Zeichenfolge eine bestimmte Größe haben? Wird es eine bekannte maximale Länge haben? Wenn Sie mit "Ja" antworten und die Zeichenfolge keine angemessenen Grenzen überschreitet, können Sie die Wertversion von StringBuilder
. Wenn Sie jedoch lange Zeichenfolgen erwarten, verwenden Sie die übliche Version.
ValueListBuilder
internal ref partial struct ValueListBuilder<T> { private Span<T> _span; private T[] _arrayFromPool; private int _pos; public ValueListBuilder(Span<T> initialSpan) { _span = initialSpan; _arrayFromPool = null; _pos = 0; } public int Length { get; set; } public ref T this[int index] { get; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(T item); public ReadOnlySpan<T> AsSpan(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose(); private void Grow(); }
Der zweite Datentyp, den ich besonders beachten möchte, ist der ValueListBuilder
Typ. Es wird verwendet, wenn Sie eine Sammlung von Elementen für kurze Zeit erstellen und zur Verarbeitung an einen Algorithmus übergeben müssen.
Zugegeben, diese Aufgabe sieht der ValueStringBuilder
Aufgabe ziemlich ähnlich. Und es wird auf ähnliche Weise gelöst:
Datei ValueListBuilder.cs coreclr / src /../ Generic / ValueListBuilder.cs
Um es klar auszudrücken, sind diese Situationen oft. Zuvor haben wir das Problem jedoch auf andere Weise gelöst. Früher haben wir eine List
, sie mit Daten gefüllt und den Verweis darauf verloren. Wenn die Methode häufig aufgerufen wird, führt dies zu einer traurigen Situation: Viele Listeninstanzen (und zugehörige Arrays) werden auf dem Heap angehalten. Jetzt ist dieses Problem gelöst: Es werden keine zusätzlichen Objekte erstellt. Wie bei ValueStringBuilder
ist dies jedoch nur für Microsoft-Programmierer gelöst: Diese Klasse verfügt über den internal
Modifikator.
Regeln und Praxis anwenden
Um den neuen Datentyp vollständig zu verstehen, müssen Sie damit spielen, indem Sie zwei oder drei oder mehr Methoden schreiben, die ihn verwenden. Es ist jedoch jetzt möglich, die wichtigsten Regeln zu lernen:
- Wenn Ihre Methode ein Eingabedatensatz verarbeitet, ohne dessen Größe zu ändern, können Sie versuchen, den
Span
Typ Span
. Wenn Sie den Puffer nicht ändern möchten, wählen Sie den Typ ReadOnlySpan
. - Wenn Ihre Methode Zeichenfolgen verarbeitet, die Statistiken berechnen oder diese Zeichenfolgen analysieren, muss
ReadOnlySpan<char>
akzeptiert werden. Muss ist eine neue Regel. Denn wenn Sie eine Zeichenfolge akzeptieren, lassen Sie jemanden einen Teilstring für Sie erstellen. - Wenn Sie ein kurzes Datenarray (nicht mehr als 10 kB) für eine Methode erstellen müssen, können Sie dies einfach mit
Span<TType> buf = stackalloc TType[size]
arrangieren. Beachten Sie, dass TType ein Werttyp sein sollte, da stackalloc
nur mit stackalloc
funktioniert.
In anderen Fällen sollten Sie sich den Memory
genauer ansehen oder klassische Datentypen verwenden.
Wie funktioniert span?
Ich möchte ein paar zusätzliche Worte dazu sagen, wie Span
funktioniert und warum es so bemerkenswert ist. Und es gibt etwas zu besprechen. Diese Art von Daten hat zwei Versionen: eine für .NET Core 2.0+ und die andere für den Rest.
Datei Span.Fast.cs, .NET Core 2.0 coreclr /.../ System / Span.Fast.cs **
public readonly ref partial struct Span<T> { /// A reference to a .NET object or a pure pointer internal readonly ByReference<T> _pointer; /// The length of the buffer based on the pointer private readonly int _length; // ... }
Datei ??? [dekompiliert]
public ref readonly struct Span<T> { private readonly System.Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; // ... }
Die Sache ist, dass in .NET Framework und .NET Core 1 * kein Garbage Collector auf spezielle Weise aktualisiert wird (im Gegensatz zu .NET Core 2.0+) und ein zusätzlicher Zeiger auf den Anfang eines Puffers in verwendet werden muss verwenden. Das bedeutet, dass Span
verwaltete .NET-Objekte intern so behandelt, als wären sie nicht verwaltet. Schauen Sie sich einfach die zweite Variante der Struktur an: Sie hat drei Felder. Der erste ist ein Verweis auf ein zerstörtes Objekt. Der zweite ist der Versatz in Bytes vom Anfang dieses Objekts, der zum Definieren des Beginns des Datenpuffers verwendet wird (in Zeichenfolgen enthält dieser Puffer Zeichen, während er in Arrays die Daten eines Arrays enthält). Schließlich enthält das dritte Feld die Anzahl der Elemente in dem Puffer, die in einer Reihe angeordnet sind.
Mal sehen, wie Span
mit Strings umgeht, zum Beispiel:
Datei MemoryExtensions.Fast.cs
coreclr /../ MemoryExtensions.Fast.cs
public static ReadOnlySpan<char> AsSpan(this string text) { if (text == null) return default; return new ReadOnlySpan<char>(ref text.GetRawStringData(), text.Length); }
Wobei string.GetRawStringData()
folgendermaßen aussieht:
Eine Datei mit der Definition der Felder coreclr /../ System / String.CoreCLR.cs
Eine Datei mit der Definition von GetRawStringData coreclr /../ System / String.cs
public sealed partial class String : IComparable, IEnumerable, IConvertible, IEnumerable<char>, IComparable<string>, IEquatable<string>, ICloneable { // // These fields map directly onto the fields in an EE StringObject. See object.h for the layout. // [NonSerialized] private int _stringLength; // For empty strings, this will be '\0' since // strings are both null-terminated and length prefixed [NonSerialized] private char _firstChar; internal ref char GetRawStringData() => ref _firstChar; }
Es stellt sich heraus, dass die Methode direkt auf das Innere der Zeichenfolge zugreift, während die Referenzspezifikation es GC ermöglicht, einen nicht verwalteten Verweis auf die Innenseite der Zeichenfolge zu verfolgen, indem sie zusammen mit der Zeichenfolge verschoben wird, wenn GC aktiv ist.
Das Gleiche gilt für Arrays: Wenn Span
erstellt wird, berechnet ein interner JIT-Code den Offset für den Anfang des Datenarrays und initialisiert Span
mit diesem Offset. Die Art und Weise, wie Sie den Offset für Zeichenfolgen und Arrays berechnen können, wurde im Kapitel über die Struktur von Objekten im Speicher (. \ ObjectsStructure.md) erläutert.
Span <T> als zurückgegebener Wert
Trotz aller Harmonie hat Span
einige logische, aber unerwartete Einschränkungen bei der Rückkehr von einer Methode. Wenn wir uns den folgenden Code ansehen:
unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = new byte[100]; return reff; }
wir können sehen, dass es logisch und gut ist. Wenn wir jedoch eine Anweisung durch eine andere ersetzen:
unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = stackalloc byte[100]; return reff; }
Ein Compiler wird es verbieten. Bevor ich sage warum, möchte ich Sie bitten, zu erraten, welche Probleme dieses Konstrukt mit sich bringt.
Nun, ich hoffe, Sie haben den Grund gedacht, erraten und vielleicht sogar verstanden. Wenn ja, haben sich meine Bemühungen, ein detailliertes Kapitel über einen [Thread-Stack] (./ThreadStack.md) zu schreiben, gelohnt. Wenn Sie einen Verweis auf lokale Variablen von einer Methode zurückgeben, die ihre Arbeit beendet, können Sie eine andere Methode aufrufen, warten, bis auch ihre Arbeit beendet ist, und dann die Werte dieser lokalen Variablen mit x [0.99] lesen.
Glücklicherweise schlägt ein Compiler beim Versuch, solchen Code zu schreiben, mit einer Warnung auf unsere Handgelenke: CS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope
. Der Compiler hat Recht, denn wenn Sie diesen Fehler umgehen, besteht in einem Plug-In die Möglichkeit, die Kennwörter anderer zu stehlen oder die Berechtigungen für die Ausführung unseres Plug-Ins zu erhöhen.
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 .