Sorpresa delle prestazioni con i tipi "as" e nullable


330

Sto solo rivedendo il capitolo 4 di C # in Depth che tratta dei tipi nullable e sto aggiungendo una sezione sull'uso dell'operatore "as", che ti permette di scrivere:

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

Ho pensato che fosse davvero pulito e che potesse migliorare le prestazioni rispetto all'equivalente C # 1, usando "is" seguito da un cast - dopotutto, in questo modo dobbiamo solo chiedere il controllo dinamico del tipo una volta, quindi un semplice controllo del valore .

Questo sembra non essere il caso, tuttavia. Di seguito ho incluso un'app di prova di esempio, che in sostanza somma tutti gli interi all'interno di un array di oggetti, ma l'array contiene molti riferimenti null e riferimenti a stringhe nonché numeri interi in scatola. Il benchmark misura il codice che dovresti usare in C # 1, il codice usando l'operatore "as" e solo per dare il via a una soluzione LINQ. Con mio stupore, il codice C # 1 è 20 volte più veloce in questo caso - e persino il codice LINQ (che mi sarei aspettato fosse più lento, dati gli iteratori coinvolti) batte il codice "as".

L'implementazione di .NET isinstper tipi nullable è davvero lenta? È l'ulteriore unbox.anyche causa il problema? C'è un'altra spiegazione per questo? Al momento sembra che dovrò includere un avvertimento contro l'utilizzo in situazioni sensibili alle prestazioni ...

risultati:

Cast: 10000000: 121
As: 10000000: 2211
LINQ: 10000000: 2143

Codice:

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

8
Perché non guardare il codice jitted? Anche il debugger VS può mostrarlo.
Anton Tykhyy,

2
Sono solo curioso, hai provato anche con CLR 4.0?
Dirk Vollmar,

1
@Anton: buon punto. Lo farà ad un certo punto (anche se questo non è in VS al momento :) @divo: Sì, ed è peggio tutto intorno. Ma poi è in beta, quindi potrebbe esserci molto codice di debug.
Jon Skeet,

1
Oggi ho imparato che puoi usare assu tipi nullable. Interessante, in quanto non può essere utilizzato su altri tipi di valore. In realtà, più sorprendente.
leppie,

3
@Lepp ha perfettamente senso che non funzioni su tipi di valore. Pensaci, astenta di eseguire il cast su un tipo e se fallisce restituisce null. Non puoi impostare i tipi di valore su null
Earlz

Risposte:


209

Chiaramente il codice macchina che il compilatore JIT può generare per il primo caso è molto più efficiente. Una regola che aiuta davvero è che un oggetto può essere decompresso solo per una variabile che ha lo stesso tipo del valore in scatola. Ciò consente al compilatore JIT di generare codice molto efficiente, nessuna conversione di valore deve essere considerata.

Il è prova operatore è facile, basta controllare se l'oggetto non è nullo ed è del tipo previsto, richiede però alcune istruzioni in codice macchina. Anche il cast è semplice, il compilatore JIT conosce la posizione dei bit di valore nell'oggetto e li utilizza direttamente. Non si verifica alcuna copia o conversione, tutto il codice macchina è in linea e richiede solo una dozzina di istruzioni. Questo doveva essere davvero efficiente in .NET 1.0 quando la boxe era comune.

Casting a int? richiede molto più lavoro. La rappresentazione del valore dell'intero inscatolato non è compatibile con il layout di memoria diNullable<int> . È richiesta una conversione e il codice è complicato a causa di possibili tipi enum inscatolati. Il compilatore JIT genera una chiamata a una funzione di supporto CLR denominata JIT_Unbox_Nullable per completare il lavoro. Questa è una funzione generica per qualsiasi tipo di valore, un sacco di codice lì per controllare i tipi. E il valore viene copiato. Difficile stimare il costo poiché questo codice è bloccato in mscorwks.dll, ma è probabile che siano centinaia le istruzioni del codice macchina.

Il metodo di estensione Linq OfType () utilizza anche l' operatore is e il cast. Questo è comunque un cast di un tipo generico. Il compilatore JIT genera una chiamata a una funzione di supporto, JIT_Unbox () che può eseguire un cast su un tipo di valore arbitrario. Non ho una grande spiegazione del perché sia ​​così lento come il cast Nullable<int>, dato che dovrebbe essere necessario meno lavoro. Ho il sospetto che ngen.exe potrebbe causare problemi qui.


16
Ok, sono convinto. Immagino di essere abituato a pensare che "sia" potenzialmente costoso a causa delle possibilità di risalire una gerarchia ereditaria - ma nel caso di un tipo di valore, non c'è possibilità di una gerarchia, quindi può essere un semplice confronto bit a bit . Penso ancora che il codice JIT per il caso nullable possa essere ottimizzato dalla JIT molto più pesantemente di quanto non sia.
Jon Skeet,

26

Mi sembra che isinstsia veramente lento sui tipi nullable. Nel metodo FindSumWithCastho cambiato

if (o is int)

per

if (o is int?)

che inoltre rallenta significativamente l'esecuzione. L'unica differenza in IL che posso vedere è quella

isinst     [mscorlib]System.Int32

viene modificato in

isinst     valuetype [mscorlib]System.Nullable`1<int32>

1
È più di questo; nel caso "cast" isinstviene seguito da un test di nullità e quindi condizionalmente un unbox.any. Nel caso nullable c'è un incondizionato unbox.any .
Jon Skeet,

Sì, risulta entrambi isinst e unbox.anysono più lenti sui tipi nullable.
Dirk Vollmar,

@Jon: puoi rivedere la mia risposta sul perché è necessario il cast. (So ​​che questo è vecchio, ma ho appena scoperto questo q e ho pensato che avrei dovuto fornire il mio 2c di ciò che so sul CLR).
Johannes Rudolph,

22

Inizialmente questo era un commento all'ottima risposta di Hans Passant, ma è passato troppo tempo, quindi voglio aggiungere alcuni bit qui:

Innanzitutto, l' asoperatore C # emetterà un'istruzione isinstIL (così come l' isoperatore). (Un'altra istruzione interessante è castclass, emessa quando si esegue un cast diretto e il compilatore sa che il controllo del runtime non può essere verificato.)

Ecco cosa isinstfa ( ECMA 335 Partition III, 4.6 ):

Formato: isinst typeTok

typeTok è un token di metadati (a typeref, typedefo typespec), che indica la classe desiderata.

Se typeTok è un tipo di valore non annullabile o un tipo di parametro generico, viene interpretato come typeTok "in box" .

Se typeTok è un tipo nullable Nullable<T>, viene interpretato come "boxed"T

Più importante:

Se il tipo effettivo (non il tipo tracciato verificatore) di obj è assegnabile al verificatore al tipo typeTok, allora ha esito isinstpositivo e obj (come risultato ) viene restituito invariato mentre la verifica traccia il suo tipo come typeTok . A differenza delle coercizioni (§1.6) e delle conversioni (§3.27), isinstnon cambia mai il tipo reale di un oggetto e conserva l'identità dell'oggetto (vedi Partizione I).

Quindi, il killer delle prestazioni non è isinstin questo caso, ma aggiuntivo unbox.any. Questo non era chiaro dalla risposta di Hans, dato che guardava solo il codice JIT. In generale, il compilatore C # emetterà un unbox.anyafter a isinst T?(ma lo ometterà nel caso lo facciate isinst T, quandoT è un tipo di riferimento).

Perché lo fa? isinst T?non ha mai l'effetto che sarebbe stato ovvio, cioè si ottiene indietro a T?. Invece, tutte queste istruzioni assicurano che hai un "boxed T"che può essere decompresso T?. Per ottenere un vero e proprio T?, abbiamo ancora bisogno di unboxing nostra "boxed T"a T?, che è il motivo per cui il compilatore emette un unbox.anydopo isinst. Se ci pensate, questo ha senso perché il "formato box" T?è solo un "boxed T"e creare castclassed isinsteseguire unbox sarebbe incoerente.

Eseguendo il backup delle scoperte di Hans con alcune informazioni dallo standard , ecco qui:

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

Quando applicato alla forma in scatola di un tipo di valore, l' unbox.anyistruzione estrae il valore contenuto in obj (di tipo O). (È equivalente a unboxseguito daldobj .) Se applicato a un tipo di riferimento, l' unbox.anyistruzione ha lo stesso effetto di castclasstypeTok.

(ECMA 335 Partition III, 4.32): unbox

In genere, unboxcalcola semplicemente l'indirizzo del tipo di valore che è già presente all'interno dell'oggetto inscatolato. Questo approccio non è possibile quando si decomprimono i tipi di valori annullabili. Poiché i Nullable<T>valori vengono convertiti in box Tsdurante l'operazione box, un'implementazione deve spesso produrre un nuovo Nullable<T>nell'heap e calcolare l'indirizzo nell'oggetto appena allocato.


Penso che l'ultima frase citata potrebbe avere un refuso; non dovrebbe "... nell'heap ..." essere "nello stack di esecuzione ?" Sembra che unboxing di nuovo in qualche nuova istanza di heap GC cambi il problema originale con uno nuovo quasi identico.
Glenn Slayden

19

È interessante notare che ho trasmesso feedback sul supporto dell'operatore dynamicessendo un ordine di grandezza più lento per Nullable<T>(simile a questo test iniziale ) - sospetto per ragioni molto simili.

Devi amare Nullable<T>. Un altro divertente è che anche se JIT individua (e rimuove) nullle strutture non annullabili, lo blocca per 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;
    }
}

Yowser. Questa è una differenza davvero dolorosa. Eek.
Jon Skeet,

Se nessun altro bene è venuto fuori da tutto questo, mi ha portato a includere avvertimenti sia per il mio codice originale che per questo :)
Jon Skeet,

So che questa è una vecchia domanda, ma potresti spiegare cosa intendi con "i punti JIT (e rimuove) nullper le strutture non annullabili"? Vuoi dire che sostituisce nullcon un valore predefinito o qualcosa del genere durante il runtime?
Justin Morgan,

2
@Justin: un metodo generico può essere utilizzato in fase di esecuzione con un numero qualsiasi di permutazioni di parametri generici ( Tecc.). I requisiti dello stack ecc. Dipendono dagli arg (quantità di spazio dello stack per un locale, ecc.), Quindi si ottiene un JIT per ogni permutazione univoca che coinvolge un tipo di valore. Tuttavia, i riferimenti hanno tutte le stesse dimensioni, quindi condividi un JIT. Mentre esegue il JIT per tipo di valore, può verificare alcuni scenari ovvi e cerca di eliminare il codice irraggiungibile a causa di cose come valori null impossibili. Non è perfetto, nota. Inoltre, sto ignorando AOT per quanto sopra.
Marc Gravell

Il test nullable senza restrizioni è ancora più lento di 2,5 ordini di grandezza, ma c'è qualche ottimizzazione in corso quando non si utilizza la countvariabile. L'aggiunta Console.Write(count.ToString()+" ");dopo la watch.Stop();in entrambi i casi rallenta gli altri test di poco meno di un ordine di grandezza, ma il test nullable senza restrizioni non viene modificato. Nota che ci sono anche cambiamenti quando testi i casi quando nullviene passato, confermando che il codice originale non sta realmente facendo il controllo null e l'incremento per gli altri test. Linqpad
Mark Hurd,

12

Questo è il risultato di FindSumWithAsAndHas sopra: testo alternativo

Questo è il risultato di FindSumWithCast: testo alternativo

I risultati:

  • Utilizzando as, verifica prima se un oggetto è un'istanza di Int32; sotto il cofano che sta usando isinst Int32(che è simile al codice scritto a mano: if (o is int)). E usando as, anche unbox incondizionatamente l'oggetto. Ed è un vero killer delle prestazioni chiamare una proprietà (è ancora una funzione nascosta), IL_0027

  • Usando il cast, verifichi prima se l'oggetto è un int if (o is int); sotto il cofano che sta usando isinst Int32. Se è un'istanza di int, puoi tranquillamente decomprimere il valore, IL_002D

In poche parole, questo è lo pseudo-codice di usare l' asapproccio:

int? x;

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

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

E questo è lo pseudo-codice dell'uso dell'approccio cast:

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

Quindi il cast ( (int)a[i], beh, la sintassi sembra un cast, ma in realtà è unboxing, cast e unboxing condividono la stessa sintassi, la prossima volta sarò pedante con la giusta terminologia) l'approccio è molto più veloce, devi solo decomprimere un valore quando un oggetto è decisamente un int. Non si può dire la stessa cosa usando un asapproccio.


11

Per mantenere aggiornata questa risposta, vale la pena ricordare che la maggior parte della discussione su questa pagina è ora discutibile con C # 7.1 e .NET 4.7 che supporta una sintassi sottile che produce anche il miglior codice IL.

L'esempio originale dell'OP ...

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

diventa semplicemente ...

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

Ho scoperto che un uso comune per la nuova sintassi è quando si scrive un tipo di valore .NET (ovvero structin C # ) che implementa IEquatable<MyStruct>(come la maggior parte dovrebbe). Dopo aver implementato il Equals(MyStruct other)metodo fortemente tipizzato , ora puoi reindirizzare con garbo la Equals(Object obj)sostituzione non tipizzata (ereditata da Object) su di essa come segue:

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

 


Appendice: il codice IL diRelease build per le prime due funzioni di esempio mostrate sopra in questa risposta (rispettivamente) è riportato qui. Mentre il codice IL per la nuova sintassi è effettivamente più piccolo di 1 byte, per lo più vince alla grande effettuando zero chiamate (contro due) ed evitando del tutto l' operazione quando possibile.unbox

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

Per ulteriori test che confermano la mia osservazione sulle prestazioni della nuova sintassi C # 7 che supera le opzioni precedentemente disponibili, vedere qui (in particolare, l'esempio "D").


9

Profilazione ulteriore:

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

}

Produzione:

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

Cosa possiamo dedurre da queste cifre?

  • In primo luogo, è-then-fuso approccio è significativamente più veloce di come approccio. 303 vs 3524
  • In secondo luogo, .Value è leggermente più lento del casting. 3524 vs 3272
  • In terzo luogo, .HasValue è leggermente più lento rispetto all'uso di manual (ovvero l'utilizzo di is ). 3524 vs 3282
  • In quarto luogo, facendo un confronto da mela a mela (cioè sia l'assegnazione di HasValue simulato che la conversione di valore simulato avvengono insieme) tra l' approccio simulato come reale e reale , possiamo vedere simulato poiché è ancora significativamente più veloce di reale come . 395 vs 3524
  • Infine, sulla base di prima e quarta conclusione, c'è qualcosa di sbagliato con il quale implementazione ^ _ ^

8

Non ho tempo di provarlo, ma potresti voler avere:

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

come

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

Stai creando un nuovo oggetto ogni volta, il che non spiega completamente il problema, ma può contribuire.


1
No, l'ho eseguito ed è leggermente più lento.
Henk Holterman,

2
Dichiarare una variabile in un posto diverso influisce in modo significativo sul codice generato solo quando la variabile viene catturata (a quel punto influenza la semantica effettiva) nella mia esperienza. Si noti che non sta creando un nuovo oggetto nell'heap, anche se sta sicuramente creando una nuova istanza di int?sullo stack utilizzando unbox.any. Ho il sospetto che sia questo il problema - la mia ipotesi è che l'IL fatta a mano potrebbe battere entrambe le opzioni qui ... anche se è anche possibile che JIT sia ottimizzato per riconoscere il caso is / cast e controllare solo una volta.
Jon Skeet,

Stavo pensando che il cast è probabilmente ottimizzato poiché è in circolazione da così tanto tempo.
James Black,

1
is / cast è un obiettivo facile per l'ottimizzazione, è un linguaggio così fastidiosamente comune.
Anton Tykhyy,

4
Le variabili locali vengono allocate nello stack quando viene creato il frame dello stack per il metodo, quindi dove si dichiara la variabile nel metodo non fa alcuna differenza. (A meno che non sia in una chiusura ovviamente, ma non è questo il caso qui.)
Guffa,

8

Ho provato l'esatto costrutto di controllo del tipo

typeof(int) == item.GetType(), che funziona alla stessa velocità della item is intversione e restituisce sempre il numero (enfasi: anche se si scrivesse un Nullable<int>array sull'array, sarà necessario utilizzarlo typeof(int)). È inoltre necessario un null != itemcontrollo aggiuntivo qui.

però

typeof(int?) == item.GetType()rimane veloce (al contrario di item is int?), ma restituisce sempre false.

Il costrutto typeof è ai miei occhi il modo più rapido per il controllo esatto del tipo, poiché utilizza RuntimeTypeHandle. Dato che i tipi esatti in questo caso non corrispondono a nullable, la mia ipotesi è che is/asbisogna fare un ulteriore sollevamento di pesi qui per assicurarsi che sia in realtà un'istanza di tipo Nullable.

E onestamente: cosa ti is Nullable<xxx> plus HasValuecompra? Niente. Puoi sempre passare direttamente al tipo (valore) sottostante (in questo caso). Puoi ottenere il valore o "no, non un'istanza del tipo che stavi chiedendo". Anche se hai scritto (int?)nullsull'array, il controllo del tipo restituirà false.


Interessante ... l'idea di usare "as" + HasValue (not is HasValue plus, note) è che sta eseguendo il controllo del tipo solo una volta invece che due volte. Fa il "check and unbox" in un solo passaggio. Sembra che dovrebbe essere più veloce ... ma chiaramente non lo è. Non sono sicuro di cosa intendi con l'ultima frase, ma non esiste un boxed int?: se inscatoli un int?valore, questo finisce come un int box o come nullriferimento.
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);
    }
}

Uscite:

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: 19/06/2010]

Nota: test precedenti sono stati eseguiti all'interno di VS, debug della configurazione, utilizzando VS2009, utilizzando Core i7 (macchina di sviluppo aziendale).

Quanto segue è stato fatto sulla mia macchina usando Core 2 Duo, usando 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

Quale versione del framework stai usando, per interesse? I risultati sul mio netbook (usando .NET 4RC) sono ancora più drammatici - le versioni che usano As sono molto peggio dei tuoi risultati. Forse l'hanno migliorato per .NET 4 RTM? Penso ancora che potrebbe essere più veloce ...
Jon Skeet,

@Michael: stavi eseguendo una build non ottimizzata o nel debugger?
Jon Skeet,

@Jon: build non ottimizzata, sotto debugger
Michael Buen,

1
@Michael: Giusto - Tendo a vedere i risultati delle prestazioni sotto un debugger come in gran parte irrilevanti :)
Jon Skeet,

@Jon: If by under debugger, ovvero all'interno di VS; sì, il benchmark precedente è stato eseguito con il debugger. Ho eseguito nuovamente il benchmark, all'interno di VS e al di fuori di esso, e compilato come debug e compilato come release. Controlla la modifica
Michael Buen,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.