346 Stimmen

Leistungsüberraschung mit "as" und nullbaren Typen

Ich überarbeite gerade Kapitel 4 von C# in Depth, das sich mit nullable types beschäftigt, und ich füge einen Abschnitt über die Verwendung des "as"-Operators hinzu, mit dem Sie schreiben können:

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

Ich dachte, dass dies wirklich nett war, und dass es die Leistung über die C# 1 Äquivalent verbessern könnte, mit "is", gefolgt von einem Cast - immerhin, auf diese Weise müssen wir nur für dynamische Typüberprüfung einmal zu fragen, und dann eine einfache Wertprüfung.

Dies scheint jedoch nicht der Fall zu sein. Ich habe unten ein Beispiel für eine Testanwendung beigefügt, die im Grunde alle Ganzzahlen in einem Objekt-Array summiert - aber das Array enthält eine Menge Null-Referenzen und String-Referenzen sowie eingeschlossene Ganzzahlen. Der Benchmark misst den Code, den Sie in C# 1 verwenden müssten, den Code, der den "as"-Operator verwendet, und - nur zum Spaß - 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 von isinst für löschbare Typen nur sehr langsam? Ist es die zusätzliche unbox.any die das Problem verursacht? Gibt es eine andere Erklärung für dieses Problem? Im Moment sieht es so aus, als müsste ich eine Warnung vor der Verwendung in leistungssensiblen Situationen einfügen...

Ergebnisse:

Besetzung: 10000000 : 121
Als: 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);
    }
}

12voto

Michael Buen Punkte 37103

Dies ist das Ergebnis von FindSumWithAsAndHas oben: alt text

Dies ist das Ergebnis von FindSumWithCast: alt text

Feststellungen:

  • 使用する as wird zuerst getestet, ob ein Objekt eine Instanz von Int32 ist; unter der Haube verwendet es isinst Int32 (was dem handgeschriebenen Code ähnelt: if (o ist int) ). Und mit as wird das Objekt auch bedingungslos entpackt. Und es ist ein echter Performance-Killer, eine Eigenschaft aufzurufen (unter der Haube ist es immer noch eine Funktion), IL_0027

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

Einfach ausgedrückt, ist dies der Pseudocode für die Verwendung von as Ansatz:

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)

So ist die Besetzung ( (int)a[i] (die Syntax sieht aus wie ein Cast, aber es ist eigentlich Unboxing, Cast und Unboxing haben die gleiche Syntax, das nächste Mal werde ich pedantisch mit der richtigen Terminologie sein) ist wirklich schneller, man musste nur einen Wert entpacken, wenn ein Objekt eindeutig ein int . Dasselbe gilt nicht für die Verwendung eines as Ansatz.

9voto

Michael Buen Punkte 37103

Weitere Profilierung:

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);
    }

}

出力します:

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 ableiten?

  • Erstens ist der "is-then-cast"-Ansatz wesentlich schneller als als Ansatz. 303 gegen 3524
  • Zweitens ist .Value geringfügig langsamer als Casting. 3524 gegenüber 3272
  • Drittens ist .HasValue geringfügig langsamer als die manuelle Verwendung von has (d. h. die Verwendung von es ). 3524 gegen 3282
  • Viertens, ein Apfel-zu-Apfel-Vergleich (d.h. sowohl die Zuweisung des simulierten HasValue als auch die Umwandlung des simulierten Value erfolgt gemeinsam) zwischen simuliert als y wirklich als Ansatz können wir sehen simuliert als ist immer noch deutlich schneller als wirklich als . 395 vs 3524
  • Schließlich, basierend auf der ersten und vierten Schlussfolgerung, ist etwas falsch mit als Umsetzung ^_^

8voto

James Black Punkte 41034

Ich habe keine Zeit, es auszuprobieren, aber vielleicht haben Sie Lust dazu:

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

als

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

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

8voto

dalo Punkte 458

Ich habe das genaue Konstrukt der Typenprüfung ausprobiert

typeof(int) == item.GetType() der genauso schnell ist wie der item is int Version, und gibt immer die Nummer zurück (Betonung: auch wenn Sie eine Nullable<int> in das Array einzufügen, müssen Sie typeof(int) ). Sie benötigen außerdem eine zusätzliche null != item siehe hier.

Allerdings

typeof(int?) == item.GetType() bleibt schnell (im Gegensatz zu item is int? ), gibt aber immer false zurück.

Das typeof-construct ist in meinen Augen der schnellste Weg für genau Typprüfung, da sie das RuntimeTypeHandle verwendet. Da die genauen Typen in diesem Fall nicht mit nullable übereinstimmen, ist meine Vermutung, is/as müssen hier zusätzliche Arbeit leisten, um sicherzustellen, dass es sich tatsächlich um eine Instanz eines nullbaren Typs handelt.

Und ganz ehrlich: Was macht Ihr is Nullable<xxx> plus HasValue Sie kaufen? Nichts. Sie können sich immer direkt an den zugrunde liegenden (Wert-)Typ wenden (in diesem Fall). Sie erhalten entweder den Wert oder "nein, keine Instanz des gesuchten Typs". Selbst wenn Sie schreiben (int?)null in das Array einfügen, wird die Typüberprüfung false zurückgeben.

7voto

Michael Buen Punkte 37103
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

[BEARBEITEN: 2010-06-19]

Hinweis: Der vorherige Test wurde in VS, Konfiguration Debug, mit VS2009, mit Core i7 (Unternehmen Entwicklungsmaschine) durchgeführt.

Die folgenden wurde auf meinem Rechner mit Core 2 Duo, mit VS2010 getan

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

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X