Unsafe.AsSpan: Span Wie werden Zeiger ersetzt?


C# ist eine unglaublich flexible Sprache. Darauf können Sie nicht nur die Backend- oder Desktop-Anwendungen schreiben. Ich verwende C# , um mit wissenschaftlichen Daten zu arbeiten, die bestimmte Anforderungen an die in der Sprache verfügbaren Tools stellen. Obwohl netcore die Agenda netcore (wenn man bedenkt, dass nach netstandard2.0 meisten Funktionen beider Sprachen und der Laufzeit nicht mehr auf netframework ), netframework ich weiterhin mit Legacy-Projekten.


In diesem Artikel beschäftige ich mich mit einer nicht offensichtlichen (aber wahrscheinlich erwünschten?) Anwendung von Span<T> und dem Unterschied zwischen der Span<T> netframework in netframework und netcore aufgrund der Besonderheiten von clr .


Haftungsausschluss 1

Die Codefragmente in diesem Artikel sind keinesfalls für die Verwendung in realen Projekten vorgesehen.


Die vorgeschlagene Lösung des (weit hergeholten?) Problems ist eher ein Proof-of-Concept.
Wenn Sie dies in Ihrem Projekt implementieren, tun Sie dies in jedem Fall auf eigene Gefahr und Gefahr.


Haftungsausschluss 2

Ich bin mir absolut sicher, dass irgendwo in jedem Fall definitiv jemand ins Knie schießen wird.


Es C# unwahrscheinlich, dass der Typ-Sicherheitsbypass in C# zu etwas Gutem führt.


Aus offensichtlichen Gründen habe ich diesen Code nicht in allen möglichen Situationen getestet, die vorläufigen Ergebnisse sehen jedoch vielversprechend aus.


Warum brauche ich Span<T> ?


Mit Spen können Sie bequemer mit Arrays nicht unmanaged Typen arbeiten und so die Anzahl der erforderlichen Zuordnungen reduzieren. Trotz der Tatsache, dass die Span-Unterstützung in BCL netframework fast vollständig fehlt, können mit System.Memory , System.Buffers und System.Runtime.CompilerServices.Unsafe mehrere Tools abgerufen werden.
Die Verwendung von Spannen in meinem Legacy-Projekt ist begrenzt, ich fand sie jedoch nicht offensichtlich, während ich auf die Typensicherheit spuckte.
Was ist diese Anwendung? In meinem Projekt arbeite ich mit Daten, die aus einem wissenschaftlichen Werkzeug stammen. Dies sind Bilder, die im Allgemeinen ein Array von T[] , wobei T einer der nicht unmanaged primitiven Typen ist, beispielsweise Int32 (auch bekannt als int ). Um diese Images korrekt auf die Festplatte zu serialisieren, muss ich das unglaublich unbequeme Legacy-Format unterstützen , das 1981 vorgeschlagen wurde und sich seitdem kaum geändert hat. Das Hauptproblem dieses Formats ist, dass es BigEndian ist . Um ein unkomprimiertes Array von T[] zu schreiben (oder zu lesen), müssen Sie die Endianess jedes Elements ändern. Die triviale Aufgabe.
Was sind einige offensichtliche Lösungen?


  1. Wir iterieren über das Array T[] , rufen BitConverter.GetBytes(T) , erweitern diese wenigen Bytes und kopieren sie in das BitConverter.GetBytes(T) .
  2. Wir iterieren über das Array T[] , führen Betrugsfälle der Form new byte[] {(byte)((x & 0xFF00) >> 8), (byte)(x & 0x00FF)}; (sollte bei Doppelbyte-Typen funktionieren), schreiben Sie in das Ziel-Array.
  3. * Aber ist T[] ein Array? Elemente sind in einer Reihe, richtig? Sie können also den ganzen Weg gehen, zum Beispiel Buffer.BlockCopy(intArray, 0, byteArray, 0, intArray.Length * sizeof(int)); . Die Methode kopiert das Array in das Array und ignoriert dabei die Typprüfung. Es ist nur notwendig, die Grenzen und die Zuordnung nicht zu übersehen. Wir mischen die Bytes als Ergebnis.
  4. * Sie sagen, dass C# (C++)++ . Aktivieren /unsafe , aktivieren fixed(int* p = &intArr[0]) byte* bPtr = (byte*)p; und jetzt können Sie die Byte-Darstellung des Quell-Arrays umgehen, die Endianess im laufenden Betrieb ändern und Blöcke auf die Festplatte schreiben ( stackalloc byte[] oder ArrayPool<byte>.Shared Für den Zwischenpuffer ArrayPool<byte>.Shared ), ohne Speicher für ein ganz neues Byte-Array ArrayPool<byte>.Shared .

Es scheint, dass Punkt 4 es Ihnen ermöglicht, alle Probleme zu lösen, aber die explizite Verwendung von unsafe Kontext und das Arbeiten mit Zeigern ist irgendwie völlig anders. Dann Span<T> uns Span<T> .


Span<T>


Span<T> sollte technisch gesehen Werkzeuge für die Arbeit mit Speicherplots bereitstellen, fast wie das Durcharbeiten von Zeigern, ohne dass das Array im Speicher „repariert“ werden muss. Solch ein GC fähiger Zeiger mit Arraygrenzen. Alles ist gut und sicher.
Eine Sache, aber - trotz der Fülle von System.Runtime.CompilerServices.Unsafe , Span<T> auf Typ Span<T> genagelt T Da spen im Wesentlichen ein Zeiger mit einer Länge von 1 + ist, was ist, wenn Sie Ihren Zeiger herausziehen, ihn in einen anderen Typ konvertieren, die Länge neu berechnen und eine neue Spanne erstellen? Glücklicherweise haben wir public Span<T>(void* pointer, int length) .
Schreiben wir einen einfachen Test:


 [Test] public void Test() { void Flip(Span<byte> span) {/*   endianess */} Span<int> x = new [] {123}; Span<byte> y = DangerousCast<int, byte>(x); Assert.AreEqual(123, x[0]); Flip(y); Assert.AreNotEqual(123, x[0]); Flip(y); Assert.AreEqual(123, x[0]); } 

Fortgeschrittenere Entwickler als ich sollte sofort erkennen, was hier falsch ist. Wird der Test fehlschlagen? Die Antwort hängt , wie es normalerweise passiert, davon ab .
In diesem Fall hängt es hauptsächlich von der Laufzeit ab. Auf netcore Test funktionieren, aber auf netframework , wie es netframework .
Interessanterweise funktioniert der Test in 100% der Fälle korrekt, wenn Sie einige der Aufsätze entfernen.
Lass es uns richtig machen.


1 Ich habe mich geirrt .


Richtige Antwort: hängt davon ab


Warum hängt das Ergebnis ab ?
Lassen Sie uns alles Unnötige entfernen und hier einen solchen Code schreiben:


 private static void Main() => Check(); private static void Check() { Span<int> x = new[] {999, 123, 11, -100}; Span<byte> y = As<int, byte>(ref x); Console.WriteLine(@"FRAMEWORK_NAME"); Write(ref x); Write(ref y); Console.WriteLine(); Write<int, int>(ref x, "Span<int> [0]"); Write<byte, int>(ref y, "Span<byte>[0]"); Console.WriteLine(); Write<int, int>(ref Offset<int, object>(ref x[0], 1), "Span<int> [0] offset by size_t"); Write<byte, int>(ref Offset<byte, object>(ref y[0], 1), "Span<byte>[0] offset by size_t"); Console.WriteLine(); GC.Collect(0, GCCollectionMode.Forced, true, true); Write<int, int>(ref x, "Span<int> [0] after GC"); Write<byte, int>(ref y, "Span<byte>[0] after GC"); Console.WriteLine(); Write(ref x); Write(ref y); } 

Die Write<T, U> -Methode akzeptiert eine Spanne vom Typ T , liest die Adresse des ersten Elements und liest durch diesen Zeiger ein Element vom Typ U Mit anderen Worten, Write<int, int>(ref x) gibt die Adresse im Speicher + die Nummer 999 aus.
Normales Write druckt ein Array.
Nun zur As<,> -Methode:


  private static unsafe Span<U> As<T, U>(ref Span<T> span) where T : unmanaged where U : unmanaged { fixed(T* ptr = span) return new Span<U>(ptr, span.Length * Unsafe.SizeOf<T>() / Unsafe.SizeOf<U>()); } 

C# Span<T>.GetPinnableReference() unterstützt diesen Datensatz mit Span<T>.GetPinnableReference() jetzt, indem implizit die Span<T>.GetPinnableReference() -Methode Span<T>.GetPinnableReference() wird.
Führen Sie diese Methode auf netframework4.8 im x64 Modus aus. Wir schauen uns an, was passiert:


 LEGACY [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|02|8C|00|00|2F|B0 999 Span<int> [0] 0x|00|00|02|8C|00|00|2F|B0 999 Span<byte>[0] 0x|00|00|02|8C|00|00|2F|B8 11 Span<int> [0] offset by size_t 0x|00|00|02|8C|00|00|2F|B8 11 Span<byte>[0] offset by size_t 0x|00|00|02|8C|00|00|2B|18 999 Span<int> [0] after GC 0x|00|00|02|8C|00|00|2F|B0 6750318 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 110, 0, 103, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] 

Anfänglich verhalten sich beide Bereiche (trotz der unterschiedlichen Typen) identisch, und der Bereich Span<byte> repräsentiert im Wesentlichen eine Byteansicht des ursprünglichen Arrays. Was du brauchst.
Okay, versuchen wir, den Beginn der Spanne auf die Größe eines IntPtr (oder 2 X int auf x64 ) zu verschieben und zu lesen. Wir erhalten das dritte Element des Arrays und die richtige Adresse. Und dann werden wir den Müll sammeln ...


 GC.Collect(0, GCCollectionMode.Forced, true, true); 

Das letzte Flag in dieser Methode fordert den GC den Heap zu komprimieren. Nach dem Aufruf von GC.Collect GC das ursprüngliche lokale Array. Span<int> spiegelt diese Änderungen wider, aber unser Span<byte> weiterhin auf die alte Adresse, bei der jetzt nicht klar ist, was. Eine großartige Möglichkeit, sich alle Knie auf einmal zu erschießen!


Schauen wir uns nun das Ergebnis des exakt gleichen Codefragments an, das auf netcore3.0.100-preview8 .


 CORE [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<int> [0] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<byte>[0] 0x|00|00|01|F2|8F|BD|C6|98 11 Span<int> [0] offset by size_t 0x|00|00|01|F2|8F|BD|C6|98 11 Span<byte>[0] offset by size_t 0x|00|00|01|F2|8F|BD|BF|38 999 Span<int> [0] after GC 0x|00|00|01|F2|8F|BD|BF|38 999 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 

Alles funktioniert und es funktioniert stabil , soweit ich sehen kann. Nach der Verdichtung ändern beide Spanier ihren Zeiger. Großartig! Aber wie kann es jetzt in einem Legacy-Projekt funktionieren?


Jit intrinsisch


Ich habe absolut vergessen, dass die Unterstützung für Spans in netcore durch intrinsik implementiert wird. Mit anderen Worten, netcore kann interne Zeiger sogar auf ein Array-Fragment erstellen und Links korrekt aktualisieren, wenn der GC es verschiebt. In netframework ist die nuget Implementierung einer Spanne eine Krücke. Tatsächlich haben wir zwei verschiedene Spen: Einer wird aus dem Array erstellt und verfolgt seine Links, der zweite aus dem Zeiger und hat keine Ahnung, auf was er zeigt. Nach dem Verschieben des ursprünglichen Arrays zeigt der Bereichszeiger weiterhin auf die Stelle, an der der Zeiger in seinen Konstruktor übergeben wurde. Zum Vergleich ist dies eine Beispielimplementierung von span in netcore :


 readonly ref struct Span<T> where T : unmanaged { private readonly ByReference<T> _pointer; //  -   private readonly int _length; } 

und im netframework :


 readonly ref struct Span<T> where T : unmanaged { private readonly Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; } 

_pinnable enthält einen Verweis auf das Array. Wenn einer an den Konstruktor übergeben wurde, enthält _byteOffset eine Verschiebung (selbst die Spanne im gesamten Array weist eine Verschiebung ungleich Null auf, die sich wahrscheinlich auf die Art und Weise bezieht, wie das Array im Speicher dargestellt wird). Wenn Sie den void* -Zeiger an den Konstruktor übergeben, wird er einfach in ein absolutes _byteOffset . Die Spanne wird eng an den Speicherbereich if(_pinnable is null) {/* */} else {/* _pinnable */} , und alle Instanzmethoden enthalten if(_pinnable is null) {/* */} else {/* _pinnable */} Bedingungen wie if(_pinnable is null) {/* */} else {/* _pinnable */} . Was tun in einer solchen Situation?


Wie es geht, ist es nicht wert, aber ich habe es trotzdem getan


Dieser Abschnitt ist verschiedenen Implementierungen netframework , die von netframework unterstützt werden und die das netframework Span<T> -> Span<U> , wobei alle erforderlichen Links netframework .
Ich warne Sie: Dies ist eine Zone abnormaler Programmierung mit möglicherweise grundlegenden Fehlern und einem undefinierten Verhalten am Ende


Methode 1: Naiv


Wie das Beispiel gezeigt hat, führt die Konvertierung von Zeigern auf netframework nicht zum gewünschten Ergebnis. Wir brauchen den _pinnable Wert. Okay, wir werden das Spiegelbild aufdecken, indem wir die privaten Felder herausziehen (sehr schlecht und nicht immer möglich). Wir werden es in einem neuen Zeitraum schreiben. Wir werden glücklich sein. Es gibt nur ein kleines Problem: spen ist eine Referenzstruktur, es kann weder ein generisches Argument sein, noch kann es in ein object gepackt werden. Standardreflexionsmethoden erfordern auf die eine oder andere Weise, die Spanne in den Referenztyp zu verschieben. Ich habe keinen einfachen Weg gefunden (auch wenn ich über private Felder nachgedacht habe).


Methode 2: Wir müssen tiefer gehen


Alles wurde bereits vor mir getan ( [1] , [2] , [3] ). Spen ist eine Struktur, unabhängig von T drei Felder belegen dieselbe Speichermenge ( auf derselben Architektur ). Was ist, wenn [FieldOffset(0)] ? Kaum gesagt als getan.


 [StructLayout(LayoutKind.Explicit)] ref struct Exchange<T, U> where T : unmanaged where U : unmanaged { [FieldOffset(0)] public Span<T> Span_1; [FieldOffset(0)] public Span<U> Span_2; } 

Wenn Sie jedoch das Programm starten (oder besser gesagt, wenn Sie versuchen, einen Typ zu verwenden), trifft TypeLoadException eine TypeLoadException - ein Generikum kann nicht LayoutKind.Explicit . Okay, es spielt keine Rolle, gehen wir den schwierigen Weg:


 [StructLayout(LayoutKind.Explicit)] public ref struct Exchange { [FieldOffset(0)] public Span<byte> ByteSpan; [FieldOffset(0)] public Span<sbyte> SByteSpan; [FieldOffset(0)] public Span<ushort> UShortSpan; [FieldOffset(0)] public Span<short> ShortSpan; [FieldOffset(0)] public Span<uint> UIntSpan; [FieldOffset(0)] public Span<int> IntSpan; [FieldOffset(0)] public Span<ulong> ULongSpan; [FieldOffset(0)] public Span<long> LongSpan; [FieldOffset(0)] public Span<float> FloatSpan; [FieldOffset(0)] public Span<double> DoubleSpan; [FieldOffset(0)] public Span<char> CharSpan; } 

Jetzt können Sie dies tun:


 private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; return exchange.ByteSpan; } 

Die Methode funktioniert nur mit einem Problem: _length Feld _length kopiert. Wenn Sie also int -> byte _length , _length die _length viermal kleiner als das reale Array.
Kein Problem:


 [StructLayout(LayoutKind.Sequential)] public ref struct Raw { public object Pinnable; public IntPtr Pointer; public int Length; } [StructLayout(LayoutKind.Explicit)] public ref struct Exchange { /* */ [FieldOffset(0)] public Raw RawView; } 

Jetzt RawView Sie über RawView auf jedes einzelne Feld zugreifen.


 private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; var exchange2 = new Exchange() { RawView = new Raw() { Pinnable = exchange.RawView.Pinnable, Pointer = exchange.RawView.Pointer, Length = exchange.RawView.Length * sizeof<int> / sizeof<byte> } }; return exchange2.ByteSpan; } 

Und es funktioniert wie es sollte , wenn Sie die Verwendung schmutziger Tricks ignorieren. Minus - Die generische Version des Konverters kann nicht erstellt werden. Sie müssen sich mit vordefinierten Typen zufrieden geben.


Methode 3: Verrückt


Wie jeder normale Programmierer automatisiere ich gerne Dinge. Die Notwendigkeit, Konverter für ein Paar nicht unmanaged Typen zu schreiben, hat mir nicht gefallen. Welche Lösung kann angeboten werden? Lassen Sie die CLR Code für Sie schreiben.


Wie erreicht man das? Es gibt verschiedene Möglichkeiten, es gibt Artikel . Kurz gesagt, der Prozess sieht folgendermaßen aus:
Erstellen Sie einen Build Builder -> erstellen Sie einen Modul Builder -> erstellen Sie einen Typ -> {Felder, Methoden usw.} -> am Ausgang erhalten wir eine Instanz von Type .
Um genau zu verstehen, wie der Typ aussehen sollte (es ist eine ref struct ), verwenden wir ein beliebiges Werkzeug vom Typ ildasm . In meinem Fall war es dotPeek .
Das Erstellen eines Type Builders sieht ungefähr so ​​aus:


 var typeBuilder = _mBuilder.DefineType($"Generated_{typeof(T).Name}", TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.ExplicitLayout // <-    | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit, typeof(ValueType)); 

Nun die Felder. Da wir Span<T> aufgrund der unterschiedlichen Längen nicht direkt nach Span<U> kopieren können, müssen wir für jede Besetzung zwei Typen erstellen


 [StructLayout(LayoutKind.Explicit)] ref struct Generated_Int32 { [FieldOffset(0)] public Span<Int32> Span; [FieldOffset(0)] public Raw Raw; } 

Hier können wir Raw mit unseren Händen deklarieren und wiederverwenden. Vergessen Sie nicht IsByRefLikeAttribute . Mit Feldern ist alles einfach:


 var spanField = typeBuilder.DefineField("Span", typeof(Span<T>), FieldAttributes.Private); spanField.SetOffset(0); var rawField = typeBuilder.DefineField("Raw", typeof(Raw), FieldAttributes.Private); rawField.SetOffset(0); 

Das ist alles, der einfachste Typ ist fertig. Zwischenspeichern Sie nun das Assembly-Modul. Benutzerdefinierte Typen werden beispielsweise im Wörterbuch zwischengespeichert ( T -> Generated_{nameof(T)} ). Wir erstellen einen Wrapper, der gemäß den beiden Typen TIn und TOut zwei Arten von Helfern generiert und die erforderlichen Operationen für die TOut ausführt. Es gibt aber einen. Wie bei der Reflexion ist es fast unmöglich, sie für Spannweiten (oder andere ref struct ) zu verwenden. Oder ich habe keine einfache Lösung gefunden . Wie soll ich sein?


Delegierte zur Rettung


Reflexionsmethoden sehen normalerweise ungefähr so ​​aus:


  object Invoke(this MethodInfo mi, object @this, object[] otherArgs) 

Sie enthalten keine Informationen zu Typen. Wenn das Verpacken (= Verpackung) für Sie akzeptabel ist, gibt es keine Probleme.
In unserem Fall müssen @this und otherArgs eine ref struct , die ich nicht otherArgs konnte.
Es gibt jedoch einen einfacheren Weg. Stellen wir uns vor, ein Typ verfügt über Getter- und Setter-Methoden (keine Eigenschaften, sondern manuell erstellte einfache Methoden).
Zum Beispiel:


 void Generated_Int32.SetSpan(Span<Int32> span) => this.Span = span; 

Zusätzlich zur Methode können wir einen Delegatentyp deklarieren (explizit im Code):


 delegate void SpanSetterDelegate<T>(Span<T> span) where T : unmanaged; 

Wir müssen dies tun, da die Standardaktion eine Action<Span<T>> -Signatur haben müsste, aber spenes nicht als generische Argumente verwendet werden können. SpanSetterDelegate ist jedoch ein absolut gültiger Delegat.
Erstellen Sie die erforderlichen Delegaten. Führen Sie dazu Standardmanipulationen durch:


 var mi = type.GetMethod("Method_Name"); // ,    public & instance var spanSetter = (SpanSetterDelegate<T>) mi.CreateDelegate(typeof(SpanSetterDelegate<T>), @this); 

Jetzt kann spanSetter beispielsweise als spanSetter(Span<T>.Empty); . Bei @this 2 handelt es sich um eine Instanz unseres dynamischen Typs, die natürlich über Activator.CreateInstance(type) , da die Struktur einen Standardkonstruktor ohne Argumente hat.


Die letzte Grenze - wir müssen dynamisch Methoden generieren.


2 Möglicherweise stellen Sie fest, dass hier etwas schief geht - Activator.CreateInstance() packt eine ref struct Instanz. Siehe Ende des nächsten Abschnitts.


Treffen Sie Reflection.Emit


Ich denke, dass Methoden mit Expression generiert werden könnten, as Die Körper unserer Trivial Getter / Setter bestehen buchstäblich aus ein paar Ausdrücken. Ich habe einen anderen, direkteren Ansatz gewählt.


Wenn Sie sich den IL- Code eines trivialen Getters ansehen, sehen Sie so etwas wie ( Debug , X86 , netframework4.8 )


 nop ldarg.0 ldfld /* - */ stloc.0 br.s /*  */ ldloc.0 ret 

Es gibt unzählige Orte zum Stoppen und Debuggen.
In der Release-Version bleibt nur das Wichtigste übrig:


 ldarg.0 ldfld /* - */ ret 

Das Nullargument der Instanzmethode lautet ... this . Daher ist in IL Folgendes geschrieben:
1) Laden Sie this herunter
2) Laden Sie den Feldwert
3) Bring es zurück


Nur was? Reflection.Emit hat eine spezielle Überladung, die neben dem Operationscode auch einen Felddeskriptorparameter enthält. Genauso wie wir es zuvor erhalten haben, zum Beispiel spanField .


 var getSpan = type.DefineMethod("GetSpan", MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.Standard, typeof(Span<T>), Array.Empty<Type>()); gen = getSpan.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ldfld, spanField); gen.Emit(OpCodes.Ret); 

Für den Setter ist es etwas komplizierter. Sie müssen dies auf den Stapel laden, das erste Argument der Funktion laden, dann die Schreibanweisung im Feld aufrufen und nichts zurückgeben:


 ldarg.0 ldarg.1 stfld /*   */ ret 

Nachdem Sie dieses Verfahren für das Raw Feld durchgeführt und die erforderlichen Delegaten deklariert haben (oder die Standarddelegierten verwendet haben), erhalten Sie einen dynamischen Typ und vier Zugriffsmethoden, aus denen die richtigen generischen Delegaten generiert werden.


Wir schreiben eine Wrapper-Klasse, die unter Verwendung von zwei generischen Parametern ( TIn , TOut ) TOut empfängt, die auf die entsprechenden (zwischengespeicherten) dynamischen Typen TIn . TOut erstellt sie ein Objekt jedes Typs und generiert nämlich vier generische Delegaten


  1. void SetSpan(Span<TIn> span) , um den Quellbereich in die Struktur zu schreiben
  2. Raw GetRaw() , um den Inhalt eines Raw GetRaw() als Raw Struktur zu lesen
  3. void SetRaw(Raw raw) , um die geänderte Raw Struktur in das zweite Objekt zu schreiben
  4. Span<TOut> GetSpan() , um die Spanne des gewünschten Typs mit korrekt eingestellten und neu berechneten Feldern zurückzugeben.

Interessanterweise müssen dynamische Typinstanzen einmal erstellt werden. Beim Erstellen eines Delegaten wird ein Verweis auf diese Objekte als @this Parameter übergeben. Hier liegt ein Verstoß gegen die Regeln vor. Activator.CreateInstance gibt ein object . Anscheinend liegt dies an der Tatsache, dass der dynamische Typ selbst nicht ref type.IsByRef funktionierte ( type.IsByRef Like == false ), aber es war möglich, ref like Felder zu erstellen. Anscheinend ist eine solche Einschränkung in der Sprache vorhanden, aber die CLR verdaut sie. Vielleicht werden hier bei nicht standardmäßiger Verwendung die Knie angeschossen. 3


Wir erhalten also eine Instanz eines generischen Typs, die vier Delegaten und zwei implizite Verweise auf Instanzen dynamischer Klassen enthält. Delegaten und Strukturen können wiederverwendet werden, wenn dieselben Castes hintereinander ausgeführt werden. Um die Leistung zu verbessern, werden wir erneut (bereits ein (TIn, TOut) -> Generator<TIn, TOut> ) für ein Paar (TIn, TOut) -> Generator<TIn, TOut> .


Der Strich ist der letzte: Wir geben Typen an, Span<TIn> -> Span<TOut>


 public Span<TOut> Cast(Span<TIn> span) { //      if (span.IsEmpty) return Span<TOut>.Empty; // Caller   ,       if (span.Length * Unsafe.SizeOf<TIn>() % Unsafe.SizeOf<TOut>() != 0) throw new InvalidOperationException(); //      // Span<TIn> _input.Span = span; _spanSetter(span); //  Raw // Raw raw = _input.Raw; var raw = _rawGetter(); var newRaw = new Raw() { Pinnable = raw.Pinnable, //    Pinnable Pointer = raw.Pointer, //   Length = raw.Length * Unsafe.SizeOf<TIn>() / Unsafe.SizeOf<TOut>() //   }; //   Raw    // Raw _output.Raw = newRaw; _rawSetter(newRaw); //     // Span<TOut> _output.Span return _spanGetter(); } 

Fazit


Manchmal - aus sportlichen Gründen - können Sie einige der Einschränkungen der Sprache umgehen und nicht standardmäßige Funktionen implementieren. Natürlich auf eigene Gefahr und Gefahr. Es ist erwähnenswert, dass Sie mit der dynamischen Methode Zeiger und unsafe / fixed Kontexte vollständig aufgeben können, was ein Bonus sein kann. Der offensichtliche Nachteil ist die Notwendigkeit der Reflexion und Typerzeugung.


Für diejenigen, die bis zum Ende gelesen haben.


Naive Benchmark-Ergebnisse

Und wie schnell ist das alles?
Ich habe die Geschwindigkeit von Kasten in einem dummen Szenario verglichen, das nicht die tatsächliche / potenzielle Verwendung solcher Kasten und Bereiche widerspiegelt, aber zumindest eine Vorstellung von Geschwindigkeit gibt.


  1. Cast_ExplicitVerwendet die Konvertierung über einen explizit deklarierten Typ wie in Methode 2 . Jede Kaste erfordert die Zuweisung von zwei kleinen Strukturen und den Zugang zu den Feldern;
  2. Cast_ILimplementiert Methode 3 , erstellt jedoch jedes Mal eine neue Instanz Generator<TIn, TOut>, was zu ständigen Suchen in Wörterbüchern führt, nachdem der erste Durchgang alle Typen generiert hat;
  3. Cast_IL_Cached Generator<TIn, TOut> , - , .. ;
  4. Buffer , , . .

int[N] N/2 .


, , . , . , , . , unmanaged .


 BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362 Intel Core i7-2700K CPU 3.50GHz (Sandy Bridge), 1 CPU, 8 logical and 4 physical cores [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Job=Clr Runtime=Clr InvocationCount=1 UnrollFactor=1 

MethodN.MeanFehlerStdDevMedianRatioRatioSD
Cast_Explicit100362.2 ns18.0967 ns52.7888 ns400.0 ns1.000,00
Cast_IL1001,237.9 ns28.5954 ns67.4027 ns1,200.0 ns3.470.51
Cast_IL_Cached100522.8 ns25.2640 ns71.2576 ns500.0 ns1.460.27
Buffer100300.0 ns0.0000 ns0.0000 ns300.0 ns0.780.11
Cast_Explicit10002,628.6 ns54.0688 ns64.3650 ns2,600.0 ns1.000,00
Cast_IL10003,216.7 ns49.8568 ns38.9249 ns3,200.0 ns1.210.03
Cast_IL_Cached10002,484.6 ns44.9717 ns37.5534 ns2,500.0 ns0.940.02
Buffer10002,055.6 ns43.9695 ns73.4631 ns2,000.0 ns0.780.03
Cast_Explicit10000002,515,157.1 ns11,809.8538 ns10,469.1278 ns2,516,050.0 ns1.000,00
Cast_IL1.000.0002,263,826.7 ns23,724.4930 ns22,191.9054 ns2,262,000.0 ns0.900.01
Cast_IL_Cached1.000.0002,265,186.7 ns19,505.5913 ns18,245.5422 ns2,266,300.0 ns0.900.01
Buffer1.000.0001,959,547.8 ns39,175.7435 ns49,544.7719 ns1,959,200.0 ns0.780.02
Cast_Explicit100000000255,751,392.9 ns2,595,107.7066 ns2,300,495.3873 ns255,298,950.0 ns1.000,00
Cast_IL100000000228,709,457.1 ns527,430.9293 ns467,553.7809 ns228,864,100.0 ns0.890.01
Cast_IL_Cached100000000227,966,553.8 ns355,027.3545 ns296,463.9203 ns227,903,600.0 ns0.890.01
Buffer100000000213,216,776.9 ns1,198,565.1142 ns1,000,856.1536 ns213,517,800.0 ns0.830.01

Acknowledgements

JetBrains ( :-)) R# VS standalone- dotPeek , . BenchmarkDotNet BenchmarkDotNet, youtube- NDC Conferences DotNext , , .


PS


3 , ref , , . ( ) . ref structs,


 static Raw Generated_Int32.GetRaw(Span<int> span) { var inst = new Generated_Int32() { Span = span }; return inst.Raw; } 

, Reflection.Emit . , ILGenerator.DeclareLocal .


 static Span<int> Generated_Int32.GetSpan(Raw raw); 


 delegate Raw GetRaw<T>(Span<T> span) where T : unmanaged; delegate Span<T> GetSpan<T>(Raw raw) where T : unmanaged; 

, , ref — . Weil ,


 var getter = type.GetMethod(@"GetRaw", BindingFlags.Static | BindingFlags.Public).CreateDelegate(typeof(GetRaw<T>), null) as GetRaw<T>; 


 Raw raw = getter(Span<TIn>.Empty); Raw newRaw = convert(raw); Span<TOut> = setter(newRaw); 

UPD01:

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


All Articles