Ich überarbeite gerade Kapitel 4 von C # in Depth, das sich mit nullbaren Typen befasst, und füge einen Abschnitt über die Verwendung des Operators "as" hinzu, in dem Sie schreiben können:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
Ich fand das wirklich ordentlich und konnte die Leistung gegenüber dem C # 1-Äquivalent verbessern, indem "is" gefolgt von einer Besetzung verwendet wurde. Schließlich müssen wir auf diese Weise nur einmal nach einer dynamischen Typprüfung und dann nach einer einfachen Wertprüfung fragen .
Dies scheint jedoch nicht der Fall zu sein. Ich habe unten eine Beispiel-Test-App eingefügt, die im Grunde alle Ganzzahlen in einem Objektarray summiert - aber das Array enthält viele Nullreferenzen und Zeichenfolgenreferenzen sowie Ganzzahlen in Kästchen. Der Benchmark misst den Code, den Sie in C # 1 verwenden müssten, den Code mit dem Operator "as" und nur für Kicks eine LINQ-Lösung. Zu meinem Erstaunen ist der C # 1-Code in diesem Fall 20-mal schneller - und sogar der LINQ-Code (von dem ich angesichts der beteiligten Iteratoren erwartet hätte, dass er langsamer ist) schlägt den "as" -Code.
Ist die .NET-Implementierung isinst
für nullfähige Typen nur sehr langsam? Ist es das zusätzliche unbox.any
, das das Problem verursacht? Gibt es eine andere Erklärung dafür? Im Moment scheint es, als müsste ich eine Warnung davor einfügen, dies in leistungsempfindlichen Situationen zu verwenden ...
Ergebnisse:
Besetzung: 10000000: 121
As: 10000000: 2211
LINQ: 10000000: 2143
Code:
using System;
using System.Diagnostics;
using System.Linq;
class Test
{
const int Size = 30000000;
static void Main()
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i+1] = "";
values[i+2] = 1;
}
FindSumWithCast(values);
FindSumWithAs(values);
FindSumWithLinq(values);
}
static void FindSumWithCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = (int) o;
sum += x;
}
}
sw.Stop();
Console.WriteLine("Cast: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithAs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithLinq(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = values.OfType<int>().Sum();
sw.Stop();
Console.WriteLine("LINQ: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
}
as
nullbare Typen verwenden können. Interessant, da es nicht für andere Werttypen verwendet werden kann. Eigentlich überraschender.as
versuchen Sie, in einen Typ umzuwandeln, und wenn dies fehlschlägt, wird null zurückgegeben. Sie könnenAntworten:
Der Maschinencode, den der JIT-Compiler für den ersten Fall generieren kann, ist eindeutig viel effizienter. Eine Regel, die dort wirklich hilft, ist, dass ein Objekt nur für eine Variable entpackt werden kann, die denselben Typ wie der umrahmte Wert hat. Dadurch kann der JIT-Compiler sehr effizienten Code generieren, es müssen keine Wertekonvertierungen berücksichtigt werden.
Der is- Operator-Test ist einfach. Überprüfen Sie einfach, ob das Objekt nicht null ist und vom erwarteten Typ ist. Es werden nur einige Anweisungen für den Maschinencode benötigt. Die Umwandlung ist auch einfach, der JIT-Compiler kennt die Position der Wertbits im Objekt und verwendet sie direkt. Es findet kein Kopieren oder Konvertieren statt, der gesamte Maschinencode ist inline und benötigt nur etwa ein Dutzend Anweisungen. Dies musste in .NET 1.0 wirklich effizient sein, als das Boxen üblich war.
Casting nach int? braucht viel mehr Arbeit. Die Wertdarstellung der Boxed Integer ist nicht mit dem Speicherlayout von kompatibel
Nullable<int>
. Eine Konvertierung ist erforderlich und der Code ist aufgrund möglicher Aufzählungstypen schwierig. Der JIT-Compiler generiert einen Aufruf einer CLR-Hilfsfunktion mit dem Namen JIT_Unbox_Nullable, um die Aufgabe zu erledigen. Dies ist eine Allzweckfunktion für jeden Werttyp, viel Code zum Überprüfen von Typen. Und der Wert wird kopiert. Die Kosten sind schwer abzuschätzen, da dieser Code in mscorwks.dll gesperrt ist, aber wahrscheinlich Hunderte von Maschinencode-Anweisungen.Die Erweiterungsmethode Linq OfType () verwendet auch den Operator is und die Umwandlung . Dies ist jedoch eine Umwandlung in einen generischen Typ. Der JIT-Compiler generiert einen Aufruf einer Hilfsfunktion, JIT_Unbox (), die eine Umwandlung in einen beliebigen Werttyp durchführen kann. Ich habe keine gute Erklärung dafür, warum es so langsam ist wie die Besetzung
Nullable<int>
, da weniger Arbeit notwendig sein sollte. Ich vermute, dass ngen.exe hier Probleme verursachen könnte.quelle
Es scheint mir, dass das
isinst
bei nullbaren Typen nur sehr langsam ist. In der Methode habeFindSumWithCast
ich mich geändertzu
Dies verlangsamt auch die Ausführung erheblich. Der einzige Unterschied in IL, den ich sehen kann, ist der
wird geändert zu
quelle
isinst
folgt auf einen Test auf Nichtigkeit und dann bedingt auf einenunbox.any
. Im nullbaren Fall gibt es eine bedingungsloseunbox.any
.isinst
undunbox.any
ist bei nullbaren Typen langsamer.Dies begann ursprünglich als Kommentar zu Hans Passants ausgezeichneter Antwort, aber es wurde zu lang, deshalb möchte ich hier ein paar Kleinigkeiten hinzufügen:
Zuerst gibt der C #
as
-Operator einenisinst
IL-Befehl aus (deris
Operator auch). (Eine weitere interessante Anweisung wirdcastclass
ausgegeben, wenn Sie eine direkte Umwandlung durchführen und der Compiler weiß, dass die Laufzeitprüfung nicht ausgelassen werden kann.)Folgendes
isinst
funktioniert ( ECMA 335 Partition III, 4.6 ):Am wichtigsten:
Der Performance-Killer ist also nicht
isinst
in diesem Fall, sondern der zusätzlicheunbox.any
. Dies ging aus Hans 'Antwort nicht hervor, da er nur den JITed-Code betrachtete. Im Allgemeinen gibt der C # -Compiler einunbox.any
Nach-a ausisinst T?
(lässt es jedoch wegisinst T
, wenn Sie dies tun , wennT
es sich um einen Referenztyp handelt).Warum macht es das?
isinst T?
hat nie den Effekt, der offensichtlich gewesen wäre, dh Sie erhalten a zurückT?
. Stattdessen stellen alle diese Anweisungen sicher, dass Sie eine haben"boxed T"
, für die eine Box ausgepackt werden kannT?
. Um ein aktuelles zu erhaltenT?
, müssen wir unser"boxed T"
to noch entpackenT?
, weshalb der Compiler einunbox.any
After ausgibtisinst
. Wenn Sie darüber nachdenken, ist dies sinnvoll, da das "Box-Format"T?
nur ein"boxed T"
istcastclass
undisinst
das Erstellen und Ausführen der Unbox inkonsistent wäre.Das Ergebnis von Hans mit einigen Informationen aus dem Standard untermauern , hier ist es:
(ECMA 335 Partition III, 4.33):
unbox.any
(ECMA 335 Partition III, 4.32):
unbox
quelle
Interessanterweise gab ich Feedback zur Bedienerunterstützung weiter,
dynamic
indem ich um eine Größenordnung langsamer warNullable<T>
(ähnlich wie bei diesem frühen Test ) - ich vermute aus sehr ähnlichen Gründen.Muss ich lieben
Nullable<T>
. Ein weiterer Spaß ist, dass die JIT zwarnull
nicht nullfähige Strukturen erkennt (und entfernt) , sie jedoch für Folgendes borktNullable<T>
:quelle
null
Entfernungen ) für nicht nullfähige Strukturen" verstehen? Meinen Sie damit, dass es zurnull
Laufzeit durch einen Standardwert oder etwas anderes ersetzt wird?T
usw.) verwendet werden. Die Anforderungen an den Stack usw. hängen von den Argumenten ab (Größe des Stack-Speicherplatzes für einen lokalen usw.), sodass Sie eine JIT für jede eindeutige Permutation mit einem Werttyp erhalten. Die Referenzen sind jedoch alle gleich groß. Teilen Sie daher eine JIT. Während der JIT pro Wert kann nach einigen offensichtlichen Szenarien gesucht werden, und es wird versucht , nicht erreichbaren Code aufgrund von Dingen wie unmöglichen Nullen zu entfernen. Es ist nicht perfekt, beachte. Außerdem ignoriere ich AOT für die oben genannten.count
Variable nicht verwenden . Das HinzufügenConsole.Write(count.ToString()+" ");
nach demwatch.Stop();
verlangsamt in beiden Fällen die anderen Tests um knapp eine Größenordnung, aber der uneingeschränkte nullbare Test wird nicht geändert. Beachten Sie, dass es auch Änderungen gibt, wenn Sie die Fälle testen, wenn sienull
bestanden werden. Wenn Sie bestätigen, dass der ursprüngliche Code nicht wirklich die Nullprüfung und das Inkrement für die anderen Tests durchführt. LinqpadDies ist das Ergebnis von FindSumWithAsAndHas oben:
Dies ist das Ergebnis von FindSumWithCast:
Ergebnisse:
Mit
as
wird zunächst geprüft, ob ein Objekt eine Instanz von Int32 ist. unter der Haube wird es verwendetisinst Int32
(ähnlich wie handgeschriebener Code: if (o is int)). Mitas
wird das Objekt auch bedingungslos entpackt. Und es ist ein echter Leistungskiller, eine Eigenschaft (es ist immer noch eine Funktion unter der Haube) IL_0027 zu nennenMit cast testen Sie zuerst, ob das Objekt ein ist
int
if (o is int)
. unter der Haube wird dies verwendetisinst Int32
. Wenn es sich um eine Instanz von int handelt, können Sie den Wert IL_002D sicher entpackenEinfach ausgedrückt ist dies der Pseudocode für die Verwendung des
as
Ansatzes:Und dies ist der Pseudocode für die Verwendung des Cast-Ansatzes:
Der Cast-
(int)a[i]
Ansatz (nun , die Syntax sieht aus wie ein Cast, aber es ist tatsächlich Unboxing, Cast und Unboxing haben dieselbe Syntax, das nächste Mal, wenn ich mit der richtigen Terminologie pedantisch bin) ist wirklich schneller, Sie müssen nur einen Wert entpacken wenn ein Objekt entschieden ein istint
. Das Gleiche gilt nicht für einenas
Ansatz.quelle
Um diese Antwort auf dem neuesten Stand zu halten, ist es erwähnenswert, dass der größte Teil der Diskussion auf dieser Seite jetzt mit C # 7.1 und .NET 4.7 diskutiert wird , die eine schlanke Syntax unterstützen, die auch den besten IL-Code erzeugt.
Das ursprüngliche Beispiel des OP ...
wird einfach ...
Ich habe festgestellt, dass eine häufige Verwendung für die neue Syntax darin besteht, einen .NET- Werttyp (dh
struct
in C # ) zu schreiben , der implementiert wirdIEquatable<MyStruct>
(wie die meisten sollten). Nach der Implementierung der stark typisiertenEquals(MyStruct other)
Methode können Sie die nicht typisierteEquals(Object obj)
Überschreibung (von der geerbt wurdeObject
) wie folgt ordnungsgemäß umleiten :Anhang: Der
Release
Build- IL- Code für die ersten beiden oben in dieser Antwort gezeigten Beispielfunktionen ist hier angegeben. Während der IL-Code für die neue Syntax tatsächlich 1 Byte kleiner ist, gewinnt er meistens groß, indem er keine Aufrufe (gegenüber zwei) tätigt und dieunbox
Operation nach Möglichkeit ganz vermeidet .Weitere Tests, die meine Bemerkung zur Leistung der neuen C # 7- Syntax untermauern, die die zuvor verfügbaren Optionen übertrifft, finden Sie hier (insbesondere Beispiel 'D').
quelle
Profilierung weiter:
Ausgabe:
Was können wir aus diesen Zahlen schließen?
quelle
Ich habe keine Zeit, es zu versuchen, aber vielleicht möchten Sie:
wie
Sie erstellen jedes Mal ein neues Objekt, das das Problem nicht vollständig erklärt, aber möglicherweise dazu beiträgt.
quelle
int?
auf dem Stapel erstellt wirdunbox.any
. Ich vermute, dass dies das Problem ist - ich vermute, dass handgefertigte IL hier beide Optionen übertreffen könnte ... obwohl es auch möglich ist, dass die JIT so optimiert ist, dass sie den Fall is / cast erkennt und nur einmal überprüft.Ich habe das genaue Konstrukt der Typprüfung ausprobiert
typeof(int) == item.GetType()
Dies ist genauso schnell wie dieitem is int
Version und gibt immer die Nummer zurück (Hervorhebung: Selbst wenn Sie eineNullable<int>
in das Array geschrieben haben, müssten Sie diese verwendentypeof(int)
). Sie benötigennull != item
hier auch einen zusätzlichen Scheck.jedoch
typeof(int?) == item.GetType()
bleibt schnell (im Gegensatz zuitem is int?
), gibt aber immer false zurück.Das Typeof-Konstrukt ist in meinen Augen der schnellste Weg zur genauen Typprüfung, da es das RuntimeTypeHandle verwendet. Da die genauen Typen in diesem Fall nicht mit nullable übereinstimmen, muss ich meiner
is/as
Meinung nach hier zusätzliches Heavy-Lifting durchführen, um sicherzustellen, dass es sich tatsächlich um eine Instanz eines nullable-Typs handelt.Und ehrlich: Was
is Nullable<xxx> plus HasValue
kaufst du dir? Nichts. Sie können jederzeit direkt zum zugrunde liegenden (Wert-) Typ wechseln (in diesem Fall). Sie erhalten entweder den Wert oder "Nein, keine Instanz des Typs, nach dem Sie gefragt haben". Selbst wenn Sie(int?)null
in das Array geschrieben haben, gibt die Typprüfung false zurück.quelle
int?
- wenn Sie einenint?
Wert boxen , endet er als Boxed Int oder alsnull
Referenz.Ausgänge:
[EDIT: 2010-06-19]
Hinweis: Der vorherige Test wurde in VS, Konfigurationsdebug, unter Verwendung von VS2009 unter Verwendung von Core i7 (Unternehmensentwicklungsmaschine) durchgeführt.
Das Folgende wurde auf meinem Computer mit Core 2 Duo unter Verwendung von VS2010 durchgeführt
quelle