Leistungsüberraschung mit "as" - und nullbaren Typen

330

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 isinstfü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);
    }
}
Jon Skeet
quelle
8
Warum nicht den Jitted Code anschauen? Sogar der VS-Debugger kann es zeigen.
Anton Tykhyy
2
Ich bin nur neugierig, haben Sie auch mit CLR 4.0 getestet?
Dirk Vollmar
1
@ Anton: Guter Punkt. Wird irgendwann funktionieren (obwohl dies im Moment nicht in VS ist :) @divo: Ja, und es ist rundum schlimmer. Aber dann ist das in der Beta, also kann es eine Menge Debugging-Code geben.
Jon Skeet
1
Heute habe ich gelernt, dass Sie asnullbare Typen verwenden können. Interessant, da es nicht für andere Werttypen verwendet werden kann. Eigentlich überraschender.
Leppie
3
@Lepp es ist durchaus sinnvoll, nicht an Werttypen zu arbeiten. Denken Sie darüber nach, asversuchen Sie, in einen Typ umzuwandeln, und wenn dies fehlschlägt, wird null zurückgegeben. Sie können
Werttypen

Antworten:

209

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.

Hans Passant
quelle
16
Okay, ich bin überzeugt. Ich glaube, ich bin es gewohnt, "ist" als potenziell teuer zu betrachten, da es möglich ist, eine Vererbungshierarchie aufzubauen - aber im Fall eines Wertetyps gibt es keine Möglichkeit einer Hierarchie, so dass es sich um einen einfachen bitweisen Vergleich handeln kann . Ich denke immer noch, dass der JIT-Code für den nullbaren Fall durch die JIT viel stärker optimiert werden könnte als er ist.
Jon Skeet
26

Es scheint mir, dass das isinstbei nullbaren Typen nur sehr langsam ist. In der Methode habe FindSumWithCastich mich geändert

if (o is int)

zu

if (o is int?)

Dies verlangsamt auch die Ausführung erheblich. Der einzige Unterschied in IL, den ich sehen kann, ist der

isinst     [mscorlib]System.Int32

wird geändert zu

isinst     valuetype [mscorlib]System.Nullable`1<int32>
Dirk Vollmar
quelle
1
Es ist mehr als das; im "Cast" -Fall isinstfolgt auf einen Test auf Nichtigkeit und dann bedingt auf einen unbox.any. Im nullbaren Fall gibt es eine bedingungslose unbox.any .
Jon Skeet
Ja, stellt sich als beides heraus isinst und unbox.anyist bei nullbaren Typen langsamer.
Dirk Vollmar
@ Jon: Sie können meine Antwort überprüfen, warum die Besetzung benötigt wird. (Ich weiß, dass dies alt ist, aber ich habe gerade dieses q entdeckt und dachte, ich sollte meine 2c von dem bereitstellen, was ich über die CLR weiß).
Johannes Rudolph
22

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 einen isinstIL-Befehl aus (der isOperator auch). (Eine weitere interessante Anweisung wird castclassausgegeben, wenn Sie eine direkte Umwandlung durchführen und der Compiler weiß, dass die Laufzeitprüfung nicht ausgelassen werden kann.)

Folgendes isinstfunktioniert ( ECMA 335 Partition III, 4.6 ):

Format: isinst typeTok

typeTok ist ein Metadatentoken (a typeref, typedefoder typespec), die gewünschte Klasse angibt.

Wenn typeTok ein nicht nullbarer Werttyp oder ein generischer Parametertyp ist, wird er als "boxed" typeTok interpretiert .

Wenn typeTok ein nullfähiger Typ ist Nullable<T>, wird er als "boxed" interpretiert.T

Am wichtigsten:

Wenn die Ist - Typ (nicht der Verifizierer verfolgt Typ) von obj ist Verifizierer zuweisbare zu dem Typ typeTok dann isinsterfolgreich ist und obj (als Ergebnis ) zurückgegeben unverändert , während Überprüfung seiner Art als Spuren typeTok . Im Gegensatz zu Zwängen (§1.6) und Konvertierungen (§3.27) isinständert sich niemals der tatsächliche Typ eines Objekts und die Objektidentität bleibt erhalten (siehe Partition I).

Der Performance-Killer ist also nicht isinstin diesem Fall, sondern der zusätzliche unbox.any. Dies ging aus Hans 'Antwort nicht hervor, da er nur den JITed-Code betrachtete. Im Allgemeinen gibt der C # -Compiler ein unbox.anyNach-a aus isinst T?(lässt es jedoch weg isinst T, wenn Sie dies tun , wenn Tes 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ück T?. Stattdessen stellen alle diese Anweisungen sicher, dass Sie eine haben "boxed T", für die eine Box ausgepackt werden kann T?. Um ein aktuelles zu erhalten T?, müssen wir unser "boxed T"to noch entpacken T?, weshalb der Compiler ein unbox.anyAfter ausgibt isinst. Wenn Sie darüber nachdenken, ist dies sinnvoll, da das "Box-Format" T?nur ein "boxed T"ist castclassund isinstdas 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

Bei Anwendung auf die unbox.anyBoxform eines Werttyps extrahiert die Anweisung den in obj (vom Typ O) enthaltenen Wert . ( Dies entspricht " unboxgefolgt von" ldobj.) Bei Anwendung auf einen Referenztyp hat die unbox.anyAnweisung den gleichen Effekt wie castclasstypeTok.

(ECMA 335 Partition III, 4.32): unbox

unboxBerechnet in der Regel einfach die Adresse des Werttyps, der bereits im umrahmten Objekt vorhanden ist. Dieser Ansatz ist nicht möglich, wenn nullfähige Werttypen entpackt werden. Da Nullable<T>Werte Tswährend der Boxoperation in Box konvertiert werden, muss eine Implementierung häufig eine neue Nullable<T>auf dem Heap erstellen und die Adresse für das neu zugewiesene Objekt berechnen.

Johannes Rudolph
quelle
Ich denke, der allerletzte zitierte Satz könnte einen Tippfehler haben; sollte "... auf dem Haufen ..." nicht "auf dem Ausführungsstapel " sein ? Scheint, als würde das Zurückpacken in eine neue GC-Heap-Instanz das ursprüngliche Problem gegen ein nahezu identisches neues austauschen.
Glenn Slayden
19

Interessanterweise gab ich Feedback zur Bedienerunterstützung weiter, dynamicindem ich um eine Größenordnung langsamer war Nullable<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 zwar nullnicht nullfähige Strukturen erkennt (und entfernt) , sie jedoch für Folgendes borkt Nullable<T>:

using System;
using System.Diagnostics;
static class Program {
    static void Main() { 
        // JIT
        TestUnrestricted<int>(1,5);
        TestUnrestricted<string>("abc",5);
        TestUnrestricted<int?>(1,5);
        TestNullable<int>(1, 5);

        const int LOOP = 100000000;
        Console.WriteLine(TestUnrestricted<int>(1, LOOP));
        Console.WriteLine(TestUnrestricted<string>("abc", LOOP));
        Console.WriteLine(TestUnrestricted<int?>(1, LOOP));
        Console.WriteLine(TestNullable<int>(1, LOOP));

    }
    static long TestUnrestricted<T>(T x, int loop) {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
    static long TestNullable<T>(T? x, int loop) where T : struct {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
}
Marc Gravell
quelle
Yowser. Das ist ein wirklich schmerzhafter Unterschied. Eek.
Jon Skeet
Wenn aus all dem kein anderes Gut hervorgegangen ist, habe ich Warnungen sowohl für meinen Originalcode als auch für diesen eingefügt :)
Jon Skeet
Ich weiß, dass dies eine alte Frage ist, aber können Sie erklären, was Sie unter "JIT-Spots (und nullEntfernungen ) für nicht nullfähige Strukturen" verstehen? Meinen Sie damit, dass es zur nullLaufzeit durch einen Standardwert oder etwas anderes ersetzt wird?
Justin Morgan
2
@Justin - Eine generische Methode kann zur Laufzeit mit einer beliebigen Anzahl von Permutationen generischer Parameter ( Tusw.) 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.
Marc Gravell
Der uneingeschränkte nullbare Test ist immer noch 2,5 Größenordnungen langsamer, aber es gibt einige Optimierungen, wenn Sie die countVariable nicht verwenden . Das Hinzufügen Console.Write(count.ToString()+" ");nach dem watch.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 sie nullbestanden 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. Linqpad
Mark Hurd
12

Dies ist das Ergebnis von FindSumWithAsAndHas oben: Alt-Text

Dies ist das Ergebnis von FindSumWithCast: Alt-Text

Ergebnisse:

  • Mit aswird zunächst geprüft, ob ein Objekt eine Instanz von Int32 ist. unter der Haube wird es verwendet isinst Int32(ähnlich wie handgeschriebener Code: if (o is int)). Mit aswird 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 nennen

  • Mit cast testen Sie zuerst, ob das Objekt ein ist int if (o is int). unter der Haube wird dies verwendet isinst Int32. Wenn es sich um eine Instanz von int handelt, können Sie den Wert IL_002D sicher entpacken

Einfach ausgedrückt ist dies der Pseudocode für die Verwendung des asAnsatzes:

int? x;

(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)

if (x.HasValue)
    sum += x.Value;    

Und dies ist der Pseudocode für die Verwendung des Cast-Ansatzes:

if (o isinst Int32)
    sum += (o unbox Int32)

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 ist int. Das Gleiche gilt nicht für einen asAnsatz.

Michael Buen
quelle
11

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 ...

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    // ...use x.Value in here
}

wird einfach ...

if (o is int x)
{
    // ...use x in here
}

Ich habe festgestellt, dass eine häufige Verwendung für die neue Syntax darin besteht, einen .NET- Werttyp (dh structin C # ) zu schreiben , der implementiert wird IEquatable<MyStruct>(wie die meisten sollten). Nach der Implementierung der stark typisierten Equals(MyStruct other)Methode können Sie die nicht typisierte Equals(Object obj)Überschreibung (von der geerbt wurde Object) wie folgt ordnungsgemäß umleiten :

public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);

 


Anhang: Der ReleaseBuild- 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 die unboxOperation nach Möglichkeit ganz vermeidet .

// static void test1(Object o, ref int y)
// {
//     int? x = o as int?;
//     if (x.HasValue)
//         y = x.Value;
// }

[0] valuetype [mscorlib]Nullable`1<int32> x
        ldarg.0
        isinst [mscorlib]Nullable`1<int32>
        unbox.any [mscorlib]Nullable`1<int32>
        stloc.0
        ldloca.s x
        call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
        brfalse.s L_001e
        ldarg.1
        ldloca.s x
        call instance !0 [mscorlib]Nullable`1<int32>::get_Value()
        stind.i4
L_001e: ret

// static void test2(Object o, ref int y)
// {
//     if (o is int x)
//         y = x;
// }

[0] int32 x,
[1] object obj2
        ldarg.0
        stloc.1
        ldloc.1
        isinst int32
        ldnull
        cgt.un
        dup
        brtrue.s L_0011
        ldc.i4.0
        br.s L_0017
L_0011: ldloc.1
        unbox.any int32
L_0017: stloc.0
        brfalse.s L_001d
        ldarg.1
        ldloc.0
        stind.i4
L_001d: ret

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').

Glenn Slayden
quelle
9

Profilierung weiter:

using System;
using System.Diagnostics;

class Program
{
    const int Size = 30000000;

    static void Main(string[] args)
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i + 1] = "";
            values[i + 2] = 1;
        }

        FindSumWithIsThenCast(values);

        FindSumWithAsThenHasThenValue(values);
        FindSumWithAsThenHasThenCast(values);

        FindSumWithManualAs(values);
        FindSumWithAsThenManualHasThenValue(values);



        Console.ReadLine();
    }

    static void FindSumWithIsThenCast(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("Is then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenValue(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 then Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (x.HasValue)
            {
                sum += (int)o;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Has then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithManualAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            bool hasValue = o is int;
            int x = hasValue ? (int)o : 0;

            if (hasValue)
            {
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Manual As: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenManualHasThenValue(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

}

Ausgabe:

Is then Cast: 10000000 : 303
As then Has then Value: 10000000 : 3524
As then Has then Cast: 10000000 : 3272
Manual As: 10000000 : 395
As then Manual Has then Value: 10000000 : 3282

Was können wir aus diesen Zahlen schließen?

  • Erstens ist der Is-Then-Cast-Ansatz erheblich schneller als der Ansatz. 303 gegen 3524
  • Zweitens ist der Wert geringfügig langsamer als das Gießen. 3524 gegen 3272
  • Drittens ist .HasValue geringfügig langsamer als die manuelle Verwendung (dh die Verwendung von is ). 3524 gegen 3282
  • Viertens: Wenn wir einen Apfel-zu-Apfel-Vergleich durchführen (dh sowohl das Zuweisen von simuliertem HasValue als auch das Konvertieren von simuliertem Wert erfolgt gemeinsam) zwischen simuliertem und realem Ansatz, können wir sehen, dass simuliert als immer noch erheblich schneller als real als ist . 395 vs 3524
  • Schließlich, basierend auf der ersten und vierten Schlussfolgerung, stimmt etwas nicht mit der Implementierung ^ _ ^
Michael Buen
quelle
8

Ich habe keine Zeit, es zu versuchen, aber vielleicht möchten Sie:

foreach (object o in values)
        {
            int? x = o as int?;

wie

int? x;
foreach (object o in values)
        {
            x = o as int?;

Sie erstellen jedes Mal ein neues Objekt, das das Problem nicht vollständig erklärt, aber möglicherweise dazu beiträgt.

James Black
quelle
1
Nein, das habe ich gemacht und es ist etwas langsamer.
Henk Holterman
2
Das Deklarieren einer Variablen an einer anderen Stelle wirkt sich nach meiner Erfahrung nur dann erheblich auf den generierten Code aus, wenn die Variable erfasst wird (an welchem ​​Punkt sie die tatsächliche Semantik beeinflusst). Beachten Sie, dass kein neues Objekt auf dem Heap erstellt wird, obwohl mit Sicherheit eine neue Instanz int?auf dem Stapel erstellt wird unbox.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.
Jon Skeet
Ich dachte, dass die Besetzung wahrscheinlich optimiert ist, da es sie schon so lange gibt.
James Black
1
is / cast ist ein leichtes Ziel für die Optimierung, es ist eine so ärgerlich verbreitete Redewendung.
Anton Tykhyy
4
Lokale Variablen werden auf dem Stapel zugewiesen, wenn der Stapelrahmen für die Methode erstellt wird. Wenn Sie also die Variable in der Methode deklarieren, spielt dies keine Rolle. (Es sei denn, es ist natürlich in einer Schließung, aber das ist hier nicht der Fall.)
Guffa
8

Ich habe das genaue Konstrukt der Typprüfung ausprobiert

typeof(int) == item.GetType()Dies ist genauso schnell wie die item is intVersion und gibt immer die Nummer zurück (Hervorhebung: Selbst wenn Sie eine Nullable<int>in das Array geschrieben haben, müssten Sie diese verwenden typeof(int)). Sie benötigen null != itemhier auch einen zusätzlichen Scheck.

jedoch

typeof(int?) == item.GetType()bleibt schnell (im Gegensatz zu item 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/asMeinung 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 HasValuekaufst 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?)nullin das Array geschrieben haben, gibt die Typprüfung false zurück.

Dalo
quelle
Interessant ... die Idee , die „als“ + HasValue von (nicht ist und HasValue, Anmerkung) ist , dass es die Typprüfung nur ist die Durchführung einmal statt zweimal. Es erledigt das "Check and Unbox" in einem einzigen Schritt. Das fühlt sich wie es sollte schneller sein ... aber es ist eindeutig nicht. Ich bin mir nicht sicher, was Sie mit dem letzten Satz meinen, aber es gibt kein Boxed int?- wenn Sie einen int?Wert boxen , endet er als Boxed Int oder als nullReferenz.
Jon Skeet
7
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);
        FindSumWithAsAndHas(values);
        FindSumWithAsAndIs(values);


        FindSumWithIsThenAs(values);
        FindSumWithIsThenConvert(values);

        FindSumWithLinq(values);



        Console.ReadLine();
    }

    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 FindSumWithAsAndHas(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 and Has: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }


    static void FindSumWithAsAndIs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As and Is: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }







    static void FindSumWithIsThenAs(object[] values)
    {
        // Apple-to-apple comparison with Cast routine above.
        // Using the similar steps in Cast routine above,
        // the AS here cannot be slower than Linq.



        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {

            if (o is int)
            {
                int? x = o as int?;
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then As: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithIsThenConvert(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {            
            if (o is int)
            {
                int x = Convert.ToInt32(o);
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then Convert: {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);
    }
}

Ausgänge:

Cast: 10000000 : 456
As and Has: 10000000 : 2103
As and Is: 10000000 : 2029
Is then As: 10000000 : 1376
Is then Convert: 10000000 : 566
LINQ: 10000000 : 1811

[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

Inside VS, Configuration: Debug

Cast: 10000000 : 309
As and Has: 10000000 : 3322
As and Is: 10000000 : 3249
Is then As: 10000000 : 1926
Is then Convert: 10000000 : 410
LINQ: 10000000 : 2018




Outside VS, Configuration: Debug

Cast: 10000000 : 303
As and Has: 10000000 : 3314
As and Is: 10000000 : 3230
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 418
LINQ: 10000000 : 1944




Inside VS, Configuration: Release

Cast: 10000000 : 305
As and Has: 10000000 : 3327
As and Is: 10000000 : 3265
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1932




Outside VS, Configuration: Release

Cast: 10000000 : 301
As and Has: 10000000 : 3274
As and Is: 10000000 : 3240
Is then As: 10000000 : 1904
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1936
Michael Buen
quelle
Welche Framework-Version verwenden Sie aus Interesse? Die Ergebnisse auf meinem Netbook (mit .NET 4RC) sind noch dramatischer - die Versionen mit As sind viel schlechter als Ihre Ergebnisse. Vielleicht haben sie es für .NET 4 RTM verbessert? Ich denke immer noch, es könnte schneller sein ...
Jon Skeet
@Michael: Haben Sie einen nicht optimierten Build oder den Debugger ausgeführt?
Jon Skeet
@ Jon: nicht optimierter Build, unter Debugger
Michael Buen
1
@ Michael: Richtig - Ich neige dazu, Leistungsergebnisse unter einem Debugger als weitgehend irrelevant anzusehen :)
Jon Skeet
@ Jon: Wenn unter Debugger, dh innerhalb von VS; Ja, der vorherige Benchmark wurde unter Debugger durchgeführt. Ich vergleiche erneut, innerhalb und außerhalb von VS, und kompilierte als Debug und als Release. Überprüfen Sie die Bearbeitung
Michael Buen