Niespodzianka wydajnościowa z typami „as” i zerowalnymi

330

Właśnie przeglądam rozdział 4 C # w Depth, który dotyczy typów zerowalnych, i dodaję sekcję o używaniu operatora „as”, który pozwala pisać:

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

Myślałem, że to naprawdę fajne i że może poprawić wydajność w stosunku do C # 1, używając „jest”, a następnie rzutowania - w końcu w ten sposób musimy poprosić tylko o dynamiczne sprawdzenie typu, a następnie proste sprawdzenie wartości .

Jednak wydaje się, że tak nie jest. Poniżej zamieściłem przykładową aplikację testową, która w zasadzie sumuje wszystkie liczby całkowite w tablicy obiektów - ale tablica zawiera wiele odwołań zerowych i odniesień łańcuchowych, a także liczb całkowitych w ramkach. Benchmark mierzy kod, którego należy użyć w języku C # 1, kod za pomocą operatora „as” i tylko dla rozwiązania LINQ. Ku mojemu zdziwieniu kod C # 1 jest w tym przypadku 20 razy szybszy - a nawet kod LINQ (który, jak się spodziewałam, byłby wolniejszy, biorąc pod uwagę zaangażowane iteratory) bije kod „jak”.

Czy implementacja .NET isinstdla typów zerowalnych jest po prostu bardzo wolna? Czy to dodatkoweunbox.any problem powoduje problem? Czy jest na to inne wytłumaczenie? W tej chwili wydaje mi się, że będę musiał dołączyć ostrzeżenie przed użyciem tego w sytuacjach wrażliwych na wydajność ...

Wyniki:

Obsada: 10000000: 121
Jako: 10000000: 2211
LINQ: 10000000: 2143

Kod:

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
źródło
8
Dlaczego nie spojrzeć na rozszyfrowany kod? Nawet debugger VS może to pokazać.
Anton Tykhyy
2
Jestem tylko ciekawy, czy testowałeś również z CLR 4.0?
Dirk Vollmar
1
@Anton: Dobra uwaga. W pewnym momencie to zrobi (chociaż obecnie nie ma tego w VS) @divo: Tak, i jest gorzej. Ale to jest wersja beta, więc może być tam dużo kodu do debugowania.
Jon Skeet
1
Dzisiaj dowiedziałem się, że możesz używać asna typach zerowalnych. Interesujące, ponieważ nie można go stosować w przypadku innych typów wartości. Właściwie bardziej zaskakujące.
leppie
3
@Lepp ma sens, aby nie działał na typach wartości. Pomyśl o tym, aspróbuje rzutować na typ, a jeśli się nie powiedzie, to zwraca null. Nie można ustawić typów wartości na null
Earlz

Odpowiedzi:

209

Oczywiście kod maszynowy, który kompilator JIT może wygenerować dla pierwszego przypadku, jest znacznie bardziej wydajny. Jedną z zasad, która naprawdę tam pomaga, jest to, że obiekt można rozpakować tylko do zmiennej, która ma ten sam typ, co wartość pudełkowa. Dzięki temu kompilator JIT może generować bardzo wydajny kod, nie trzeba brać pod uwagę konwersji wartości.

jest badanie operator jest proste, wystarczy sprawdzić, czy obiekt nie jest nieważna i jest oczekiwanego typu, ale trwa kilka instrukcje kodu maszynowego. Rzutowanie jest również łatwe, kompilator JIT zna położenie bitów wartości w obiekcie i używa ich bezpośrednio. Kopiowanie lub konwersja nie występuje, cały kod maszynowy jest wbudowany i zajmuje około tuzina instrukcji. To musiało być naprawdę wydajne w .NET 1.0, gdy boks był powszechny.

Przesyłam na int? zajmuje dużo więcej pracy. Reprezentacja wartości pudełkowatej liczby całkowitej jest niezgodna z układem pamięci Nullable<int>. Wymagana jest konwersja, a kod jest trudny ze względu na możliwe typy wyliczeń w pudełku. Kompilator JIT generuje wywołanie funkcji pomocniczej CLR o nazwie JIT_Unbox_Nullable, aby wykonać zadanie. Jest to funkcja ogólnego przeznaczenia dla dowolnego typu wartości, z dużą ilością kodu do sprawdzania typów. Wartość jest kopiowana. Trudno oszacować koszt, ponieważ ten kod jest zamknięty w pliku mscorwks.dll, ale prawdopodobnie setki instrukcji kodu maszynowego.

Metoda rozszerzenia Linq OfType () również używa operatora is i rzutowania. Jest to jednak obsada typu ogólnego. Kompilator JIT generuje wywołanie funkcji pomocniczej JIT_Unbox (), która może wykonać rzutowanie na dowolny typ wartości. Nie mam wielkiego wyjaśnienia, dlaczego jest tak powolny jak obsada Nullable<int>, biorąc pod uwagę, że mniej pracy powinno być konieczne. Podejrzewam, że ngen.exe może powodować problemy tutaj.

Hans Passant
źródło
16
Okej, jestem przekonany. Wydaje mi się, że przywykłem do myślenia, że ​​„jest” jako potencjalnie kosztowne z powodu możliwości przejścia do hierarchii dziedziczenia - ale w przypadku typu wartości nie ma możliwości hierarchii, więc może to być proste porównanie bitowe . Nadal uważam, że kod JIT dla pustej skrzynki może być zoptymalizowany przez JIT o wiele bardziej niż jest.
Jon Skeet
26

Wydaje mi się, że w przypadku isinsttypów zerowalnych jest to bardzo powolne. W metodzie FindSumWithCastzmieniłem

if (o is int)

do

if (o is int?)

co również znacznie spowalnia wykonanie. Jedyne różnice w IL, jakie widzę, to to

isinst     [mscorlib]System.Int32

zmienia się na

isinst     valuetype [mscorlib]System.Nullable`1<int32>
Dirk Vollmar
źródło
1
To coś więcej; w „szarego” wypadku isinstnastępuje test nieważności a następnie warunkowounbox.any . W przypadku zerowym istnieje bezwarunkowy unbox.any .
Jon Skeet
Tak, okazuje się oba isinst i unbox.anysą wolniejsze na typach zerowalnych.
Dirk Vollmar
@Jon: Możesz przejrzeć moją odpowiedź dotyczącą tego, dlaczego potrzebna jest obsada. (Wiem, że to jest stare, ale właśnie odkryłem to q i pomyślałem, że powinienem dostarczyć moje 2c tego, co wiem o CLR).
Johannes Rudolph
22

Zaczęło się to od komentarza do doskonałej odpowiedzi Hansa Passanta, ale trwało zbyt długo, więc chcę tutaj dodać kilka bitów:

Po pierwsze, asoperator C # wyśle isinstinstrukcję IL (podobnie jak isoperator). (Kolejna interesująca instrukcja jest castclassemitowana podczas bezpośredniego przesyłania, a kompilator wie, że nie można pominąć sprawdzania czasu wykonywania).

Oto, co isinstrobi ( ECMA 335 Partition III, 4.6 ):

Format: isinst typeTok

typeTok jest wyrazem metadanych (a typeref, typedefi typespec), co wskazuje pożądaną klasę.

Jeśli typeTok jest niedozwolonym typem wartości lub ogólnym typem parametru, jest interpretowany jako „boxed” typeTok .

Jeśli typeTok jest typem zerowalnym Nullable<T>, jest interpretowany jako „pudełkowany”T

Najważniejsze:

Jeśli rzeczywisty typ obiektu (nie typ śledzenia przez weryfikatora) obj jest weryfikatorem-przypisywalnym- typowi typeTok, to się isinstpowiedzie i obiekt obj (jako wynik ) zostanie zwrócony bez zmian, podczas gdy weryfikacja śledzi jego typ jako typeTok . W przeciwieństwie do koercji (§ 1.6) i konwersji (§3.27), isinstnigdy nie zmienia faktycznego typu obiektu i zachowuje tożsamość obiektu (patrz Partycja I).

Tak więc zabójca wydajności nie jest isinstw tym przypadku, ale dodatkowym unbox.any. Nie było to jasne w odpowiedzi Hansa, gdy spojrzał tylko na kod JITed. Ogólnie rzecz biorąc, kompilator C # będzie emitował unbox.anypo nim isinst T?(ale pominie go w przypadku isinst T, gdy Tto zrobisz , kiedy jest typem odniesienia).

Dlaczego to robi? isinst T?nigdy nie ma efektu, który byłby oczywisty, tzn. odzyskasz T?. Zamiast tego, wszystkie te instrukcje zapewniają, że masz do "boxed T"rozpakowania T?. Aby uzyskać rzeczywisty T?, nadal musimy rozpakować nasze "boxed T"do T?, i dlatego kompilator emituje unbox.anypo isinst. Jeśli się nad tym zastanowić, ma to sens, ponieważ „format skrzynki” T?to po prostu "boxed T"a tworzenie castclassi isinstwykonywanie rozpakowywania byłoby niespójne.

Kopię zapasową odkrycia Hansa z pewnymi informacjami ze standardu , oto:

(ECMA 335 Partition III, 4.33): unbox.any

Po zastosowaniu do postaci pudełkowej typu wartości unbox.anyinstrukcja wyodrębnia wartość zawartą w obiekcie obj (typu O). (Odpowiada to, unboxpo której następuje ldobj). Po zastosowaniu do typu referencyjnego unbox.anyinstrukcja ma taki sam efekt jak castclasstypeTok.

(ECMA 335 partycja III, 4.32): unbox

Zwykle unboxpo prostu oblicza adres typu wartości, który jest już obecny wewnątrz obiektu w ramce. To podejście nie jest możliwe, gdy rozpakowujesz typy wartości dopuszczających wartości zerowe. Ponieważ Nullable<T>wartości są konwertowane Tsna ramkę podczas operacji skrzynki, implementacja często musi wytworzyć nową Nullable<T>na stercie i obliczyć adres do nowo przydzielonego obiektu.

Johannes Rudolph
źródło
Myślę, że ostatnie cytowane zdanie może mieć literówkę; nie powinno „… na stercie …” być „na stosie wykonania ?” Wygląda na to, że rozpakowanie z powrotem do nowej instancji sterty GC zamienia pierwotny problem na prawie identyczny nowy.
Glenn Slayden
19

Co ciekawe, przekazałem informację zwrotną na temat wsparcia operatora, dynamicponieważ byłem wolniejszy o rząd wielkości Nullable<T>(podobnie jak w tym wczesnym teście ) - podejrzewam z bardzo podobnych powodów.

Musisz kochać Nullable<T>. Kolejną zabawną rzeczą jest to, że chociaż JIT nullrozpoznaje (i usuwa) struktury niedozwolone, to rozdziela go na 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
źródło
Yowser. To naprawdę bolesna różnica. Eek.
Jon Skeet
Jeśli z tego wszystkiego nie wyszło żadne inne dobro, doprowadziło mnie to do włączenia ostrzeżeń zarówno do mojego oryginalnego kodu, jak i tego :)
Jon Skeet,
Wiem, że to stare pytanie, ale czy mógłbyś wyjaśnić, co masz na myśli przez „punkty JIT (i usuwa) nulldla struktur, które nie mają wartości zerowych”? Czy masz na myśli, że zastępuje nullon wartość domyślną lub coś w czasie wykonywania?
Justin Morgan
2
@Justin - ogólna metoda może być używana w czasie wykonywania z dowolną liczbą permutacji parametrów ogólnych ( Titp.). Wymagania dotyczące stosu itp. Zależą od argumentów (ilość miejsca stosu dla lokalnego itp.), Więc otrzymujesz jeden JIT dla każdej unikalnej permutacji obejmującej typ wartości. Jednak referencje są tego samego rozmiaru, więc udostępnij JIT. Wykonując JIT według wartości, może sprawdzić kilka oczywistych scenariuszy i próbuje wyciąć nieosiągalny kod z powodu takich rzeczy, jak niemożliwe wartości zerowe. Uwaga, to nie jest idealne. Ponadto ignoruję AOT dla powyższego.
Marc Gravell
Nieograniczony test dopuszczający zerowanie jest nadal wolniejszy o 2,5 rzędu wielkości, ale optymalizacja jest przeprowadzana, gdy nie używasz countzmiennej. Dodanie Console.Write(count.ToString()+" ");po watch.Stop();obu przypadkach spowalnia pozostałe testy o prawie rząd wielkości, ale nieograniczony test zerowania nie ulega zmianie. Zauważ, że nastąpiły również zmiany podczas testowania przypadków, w których nullprzekazanie zostało potwierdzone, potwierdzając, że oryginalny kod tak naprawdę nie wykonuje sprawdzania wartości zerowej i przyrostu dla innych testów. Linqpad
Mark Hurd
12

Jest to wynik FindSumWithAsAndHas powyżej: alternatywny tekst

Jest to wynik FindSumWithCast: alternatywny tekst

Wyniki:

  • Używając as, najpierw sprawdza, czy obiekt jest instancją Int32; pod maską używa isinst Int32(który jest podobny do odręcznego kodu: if (o jest int)). I przy użyciu asbezwarunkowo rozpakowuje również obiekt. I jest prawdziwym zabójcą wydajności nazywanie właściwości (wciąż jest to funkcja pod maską), IL_0027

  • Za pomocą rzutowania najpierw testujesz, czy obiekt jest int if (o is int); używa tego pod maską isinst Int32. Jeśli jest to instancja int, możesz bezpiecznie rozpakować wartość IL_002D

Mówiąc najprościej, jest to pseudo-kod użycia asmetody:

int? x;

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

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

A to pseudo-kod użycia metody rzutowania:

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

Więc rzutowanie ( (int)a[i]no cóż, składnia wygląda jak rzutowanie, ale tak naprawdę rozpakowywanie, rzutowanie i rozpakowywanie mają tę samą składnię, następnym razem będę pedantyczny z właściwą terminologią) podejście jest naprawdę szybsze, wystarczy tylko rozpakować wartość gdy obiekt jest zdecydowanie int. Tego samego nie można powiedzieć o zastosowaniu aspodejścia.

Michael Buen
źródło
11

Aby zachować aktualność tej odpowiedzi, warto wspomnieć, że większość dyskusji na tej stronie jest teraz dyskusyjna dzięki C # 7.1 i .NET 4.7, które obsługują wąską składnię, która również wytwarza najlepszy kod IL.

Oryginalny przykład PO ...

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

staje się po prostu ...

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

Odkryłem, że jednym z powszechnych zastosowań nowej składni jest pisanie typu wartości .NET (tj. structW języku C # ), który implementuje IEquatable<MyStruct>(jak większość powinna). Po zaimplementowaniu metody o silnym typie Equals(MyStruct other)możesz teraz z wdziękiem przekierować do niej niepopisane Equals(Object obj)zastąpienie (dziedziczone z Object) w następujący sposób:

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

 


Dodatek: Tutaj podano kod Releasekompilacji IL dla pierwszych dwóch przykładowych funkcji pokazanych powyżej w tej odpowiedzi (odpowiednio). Chociaż kod IL dla nowej składni jest rzeczywiście o 1 bajt mniejszy, to najczęściej wygrywa duży, wykonując zero wywołań (w porównaniu do dwóch) i unikając unboxoperacji, jeśli to możliwe.

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

Dalsze testy, które potwierdzają moją uwagę na temat wydajności nowej składni C # 7 przewyższającej wcześniej dostępne opcje, zobacz tutaj (w szczególności przykład „D”).

Glenn Slayden
źródło
9

Dalsze profilowanie:

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

}

Wynik:

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

Co możemy wywnioskować z tych liczb?

  • Po pierwsze, jest-then-cast podejście jest znacznie szybszy niż w podejściu. 303 vs 3524
  • Po drugie, .Value jest nieznacznie wolniejszy niż casting. 3524 vs 3272
  • Po trzecie, .HasValue jest nieznacznie wolniejszy niż używanie manualne (tzn. Używanie is ). 3524 vs 3282
  • Po czwarte, robi jabłko-do-jabłek porównanie (czyli zarówno przypisanie symulowanych hasValue i konwersja symulowanych Wartość dzieje razem) pomiędzy symulowane jak i prawdziwe jak podejścia, widzimy symulowane jak jest nadal znacznie szybciej niż prawdziwe jak . 395 vs 3524
  • Wreszcie, na podstawie pierwszego i czwartego wniosku, że jest coś złego w realizacji ^ _ ^
Michael Buen
źródło
8

Nie mam czasu, aby spróbować, ale możesz chcieć mieć:

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

tak jak

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

Za każdym razem tworzysz nowy obiekt, który nie do końca wyjaśni problem, ale może się przydać.

James Black
źródło
1
Nie, uruchomiłem to i jest nieco wolniejszy.
Henk Holterman
2
Zadeklarowanie zmiennej w innym miejscu wpływa znacząco na wygenerowany kod tylko wtedy, gdy zmienna jest przechwytywana (w tym momencie wpływa na faktyczną semantykę) z mojego doświadczenia. Zauważ, że nie tworzy nowego obiektu na stercie, chociaż z pewnością tworzy nową instancję int?na stosie przy użyciu unbox.any. Podejrzewam, że na tym polega problem - domyślam się, że ręcznie wykonana IL mogłaby pokonać obie opcje tutaj ... chociaż możliwe jest również, że JIT jest zoptymalizowany do rozpoznawania w przypadku is / cast i sprawdza tylko raz.
Jon Skeet
Myślałem, że obsada jest prawdopodobnie zoptymalizowana, ponieważ istnieje już od tak dawna.
James Black
1
is / cast jest łatwym celem optymalizacji, jest to irytująco powszechny idiom.
Anton Tykhyy,
4
Zmienne lokalne są przydzielane na stosie podczas tworzenia ramki stosu dla metody, więc deklaracja zmiennej w metodzie nie ma żadnego znaczenia. (Chyba że jest to oczywiście zamknięcie, ale tutaj tak nie jest.)
Guffa,
8

Wypróbowałem dokładną konstrukcję sprawdzania typu

typeof(int) == item.GetType(), który działa tak szybko, jak item is intwersja i zawsze zwraca liczbę (podkreślenie: nawet jeśli napisałeś Nullable<int>tablicę, musisz użyć typeof(int)). Potrzebujesz także dodatkowego null != itemczeku tutaj.

jednak

typeof(int?) == item.GetType()pozostaje szybki (w przeciwieństwie do item is int?), ale zawsze zwraca false.

Typeof-construct jest moim zdaniem najszybszym sposobem na dokładne sprawdzenie typu, ponieważ wykorzystuje on RuntimeTypeHandle. Ponieważ dokładne typy w tym przypadku nie pasują do wartości zerowej, sądzę, is/asże muszę tutaj wykonać dodatkowe podnoszenie ciężarów, upewniając się, że jest to rzeczywiście przypadek typu zerowalnego.

I szczerze mówiąc: co is Nullable<xxx> plus HasValuekupujesz? Nic. Zawsze możesz przejść bezpośrednio do bazowego (wartości) typu (w tym przypadku). Dostajesz wartość lub „nie, nie instancję typu, o który prosiłeś”. Nawet jeśli napisałeś (int?)nulldo tablicy, sprawdzanie typu zwróci false.

dalo
źródło
Ciekawe ... Pomysł użycia „as” + HasValue (nie jest plus HasValue, uwaga) polega na tym, że sprawdza typ tylko raz zamiast dwa razy. Wykonuje „zaznaczanie i rozpakowywanie” w jednym kroku. To wydaje się, że powinno być szybciej ... ale najwyraźniej nie jest. Nie jestem pewien, co masz na myśli przez ostatnie zdanie, ale nie ma czegoś takiego jak boks int?- jeśli boksujesz int?wartość, to kończy się jako boks int lub nullodniesienie.
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);
    }
}

Wyjścia:

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]

Uwaga: poprzedni test został przeprowadzony wewnątrz VS, debugowania konfiguracji, przy użyciu VS2009, przy użyciu Core i7 (firmowa maszyna programistyczna).

Poniższe czynności zostały wykonane na moim komputerze przy użyciu Core 2 Duo i VS2010

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
źródło
Której wersji ramowej używasz poza zainteresowaniem? Wyniki na moim netbooku (przy użyciu .NET 4RC) są jeszcze bardziej dramatyczne - wersje używające As są znacznie gorsze od twoich wyników. Może ulepszyli go dla .NET 4 RTM? Nadal uważam, że może być szybciej ...
Jon Skeet
@Michael: Czy korzystałeś z niezoptymalizowanej wersji lub działasz w debuggerze?
Jon Skeet,
@Jon: niezoptymalizowana wersja, w trakcie debuggera
Michael Buen,
1
@Michael: Racja - wyniki debuggera wydają mi się w dużej mierze nieistotne :)
Jon Skeet
@Jon: Jeśli przez debugger, czyli wewnątrz VS; tak, poprzedni test został wykonany podczas debuggera. Znowu testuję, zarówno wewnątrz VS, jak i poza nim, i skompilowałem jako debugowanie i skompilowałem jako wydanie. Sprawdź zmianę
Michael Buen