
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 1Die 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 2Ich 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?
- Wir iterieren über das Array
T[]
, rufen BitConverter.GetBytes(T)
, erweitern diese wenigen Bytes und kopieren sie in das BitConverter.GetBytes(T)
. - 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. - * 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. - * 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) {} 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;
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
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");
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
void SetSpan(Span<TIn> span)
, um den Quellbereich in die Struktur zu schreibenRaw GetRaw()
, um den Inhalt eines Raw GetRaw()
als Raw
Struktur zu lesenvoid SetRaw(Raw raw)
, um die geänderte Raw
Struktur in das zweite Objekt zu schreibenSpan<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) {
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-ErgebnisseUnd 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.
Cast_Explicit
Verwendet 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;Cast_IL
implementiert 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;Cast_IL_Cached
Generator<TIn, TOut>
, - , .. ;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
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: