Domanda

Sono solo rivedere il capitolo 4 di C # in profondità che si occupa di tipi nullable, e io sono l'aggiunta di una sezione sull'utilizzo del "come" operatore, che permette di scrivere:

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

Ho pensato che questo era veramente pulito, e che potrebbe migliorare le prestazioni sopra la C # 1 equivalente, utilizzando "è" seguito da un cast - dopo tutto, in questo modo abbiamo solo bisogno di chiedere il tipo dinamico il controllo una volta, e poi una semplice controllo del valore.

Questo non sembra essere il caso, tuttavia. Ho incluso un'applicazione di test di esempio, che riassume praticamente tutti gli interi all'interno di una matrice di oggetti - ma la matrice contiene molti riferimenti nulli e riferimenti stringa così come numeri interi in scatola. Le misure di riferimento il codice che avrebbe dovuto utilizzare in C # 1, il codice utilizzando il "come" operatore, e solo per calci una soluzione LINQ. Con mio grande stupore, il codice C # 1 è di 20 volte più veloce in questo caso -. E anche il codice LINQ (che mi sarei aspettato di essere più lento, dati i iteratori coinvolti) batte il "come" codice

E 'la realizzazione di .NET isinst per i tipi nullable solo veramente lento? È la unbox.any aggiuntivo che causa il problema? C'è un'altra spiegazione per questo? Al momento ci si sente come sto andando a includere un avvertimento contro l'uso di questo in termini di prestazioni situazioni delicate ...

Risultati:

  

Cast: 10000000: 121
  Come: 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);
    }
}
È stato utile?

Soluzione

Chiaramente il codice macchina il compilatore JIT può generare per il primo caso è molto più efficiente. Una regola che davvero aiuta v'è che un oggetto può essere solo unboxing a una variabile che ha lo stesso tipo come il valore boxed. Ciò consente il compilatore JIT per generare il codice molto efficiente, senza conversioni di valore devono essere considerati.

Il è di prova all'operatore è semplice, basta controllare se l'oggetto non è nullo ed è del tipo previsto, prende ma un paio di istruzioni in codice macchina. Il cast è anche facile, il compilatore JIT conosce la posizione dei bit valore nell'oggetto e li usa direttamente. Riproduzione vietata o la conversione si verifica, tutto il codice macchina è in linea e prende, ma circa una dozzina di istruzioni. Questo doveva essere realmente efficace nel .NET 1.0 quando la boxe era comune.

Casting a int? richiede molto più lavoro. La rappresentazione del valore intero inscatolato non è compatibile con il layout di memoria di Nullable<int>. Una conversione è richiesto e il codice è difficile a causa di possibili tipi enum scatolati. Il compilatore JIT genera una chiamata a una funzione CLR aiutante di nome JIT_Unbox_Nullable per ottenere il lavoro fatto. Questa è una funzione di uso generale per qualsiasi tipo di valore, un sacco di codice lì per controllare i tipi. E il valore viene copiato. Difficile stimare il costo dal momento che questo codice è rinchiuso all'interno mscorwks.dll, ma centinaia di istruzioni in codice macchina è probabile.

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

Altri suggerimenti

Mi sembra che la isinst è solo molto lento sui tipi nullable. Nel metodo FindSumWithCast ho cambiato

if (o is int)

a

if (o is int?)

che anche rallenta in modo significativo l'esecuzione. L'unica differenc in IL posso vedere è che

isinst     [mscorlib]System.Int32

viene cambiato in

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

Questa originariamente iniziato come un commento alla risposta eccellente di Hans Passant, ma faceva troppo a lungo in modo voglio aggiungere qualche bit qui:

In primo luogo, il C # as operatore emette un'istruzione IL isinst (così fa l'operatore is). (Un'altra istruzione interessante è castclass, emited quando si esegue un cast diretto e il compilatore sa che il controllo runtime non può essere ommited.)

Ecco cosa isinst fa ( ECMA 335 di ripartizione III, 4.6 ):

  

Formato: isinst typeTok

     

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

     

Se typeTok è un tipo di valore non null o un tipo di parametro generico viene interpretato come “scatola” typeTok .

     

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

La cosa più importante:

  

Se il tipo effettivo (non il tipo cingolato verificatore) di obj verificatore assegnabile a il tipo typeTok poi isinst riesce e obj (come risultato ) viene restituito invariato, mentre la verifica tiene traccia il suo tipo come typeTok . A differenza di coercizioni (§1.6) e conversioni (§3.27), isinst non cambia mai il tipo effettivo di un oggetto e conserva l'identità di oggetto (vedi Partizione I).

Quindi, il killer delle prestazioni non è isinst in questo caso, ma la unbox.any supplementare. Questo non era chiaro dalla risposta Hans', mentre guardava solo il codice JITed. In generale, il compilatore C # emetterà un unbox.any dopo un isinst T? (ma omettere nel caso in cui si fa isinst T, quando T è un tipo di riferimento).

Perché si fa? isinst T? mai ha l'effetto che sarebbe stato ovvio, vale a dire che si ottiene indietro un T?. Invece, tutte le istruzioni garantire è che si dispone di un "boxed T" che può essere unboxed a T?. Per ottenere un vero e proprio T?, abbiamo ancora bisogno di unboxing nostro "boxed T" a T?, motivo per cui il compilatore emette un unbox.any dopo isinst. Se ci pensate, questo ha un senso perché il "formato di box" per T? è solo un "boxed T" e rendendo castclass e isinst eseguire l'Unbox sarebbe incoerente.

Backup ritrovamento Hans' con alcune informazioni dal standard di , qui va:

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

  

Quando applicato alla forma scatolata di un tipo di valore, l'istruzione unbox.any estrae il valore contenuto all'interno obj (di tipo O). (È equivalente a unbox seguito da ldobj.) Quando viene applicato a un tipo di riferimento, l'istruzione unbox.any ha lo stesso effetto come castclass typeTok.

(ECMA 335 Partition III, 4.32): unbox

  

Tipicamente, unbox calcola semplicemente l'indirizzo del tipo di valore che è già presente all'interno dell'oggetto scatolato. Questo approccio non è possibile quando unboxing tipi di valore nullable. Poiché i valori Nullable<T> vengono convertiti Ts scatola durante l'operazione di casella, un'implementazione spesso deve produrre una nuova Nullable<T> sul mucchio e calcolare l'indirizzo all'oggetto appena allocato.

È interessante notare, ho passato sul feedback sul supporto operatore tramite dynamic essendo un ordine di grandezza più lento per Nullable<T> (simile a questa prima prova ) - ho il sospetto che per ragioni molto simili

.

Gotta love Nullable<T>. Un altro divertente è che, anche se le macchie JIT (e revocare) null per le strutture non annullabili, è Borks 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;
    }
}

Questo è il risultato di FindSumWithAsAndHas sopra: alt text

Questo è il risultato di FindSumWithCast: alt text

Risultati:

  • Utilizzando as, esso prova in primo luogo se un oggetto è un'istanza di Int32; sotto il cofano esso utilizza isinst Int32 (che è simile al codice scritta a mano: se (o è int)). E l'utilizzo di as, ma anche Unbox incondizionatamente l'oggetto. Ed è un vero e proprio spettacolo-killer per chiamare una proprietà (è ancora una funzione sotto il cofano), IL_0027

  • Utilizzando cast, si prova prima se l'oggetto è un int if (o is int); sotto il cofano questo sta usando isinst Int32. Se si tratta di un 'istanza di int, allora si può tranquillamente Unbox il valore, IL_002D

In poche parole, questa è la pseudo-codice di utilizzare approccio as:

int? x;

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

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

E questa è la pseudo-codice di utilizzare approccio Cast:

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

Quindi il cast ((int)a[i], ben la sintassi si presenta come un fuso, ma in realtà è unboxing, cast e la quota di unboxing la stessa sintassi, la prossima volta sarò pedante con la terminologia destra) approccio è davvero più veloce, è necessaria solo di unboxing un valore quando un oggetto è decisamente un int. La stessa cosa non si può dire di utilizzare un approccio as.

Profiling ulteriormente:

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

}

Output:

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

Che cosa possiamo dedurre da questi dati?

  • approccio In primo luogo, viene-then-cast è significativamente più veloce di come approccio. 303 vs 3524
  • In secondo luogo, .Value è marginalmente più lento di fusione. 3524 vs 3272
  • In terzo luogo, .HasValue è marginalmente più lento rispetto all'utilizzo manuale ha (cioè. Con ). 3524 vs 3282
  • In quarto luogo, facendo un confronto mela-a-mele (cioè sia assegnazione di HasValue simulato e la conversione di valore simulato accade insieme) tra approccio simulato come e reale come, noi può vedere simulato come è ancora significativamente più veloce di reale . 395 vs 3524
  • Infine, sulla base di prima e quarta conclusione, c'è qualcosa di sbagliato con il come implementazione ^ _ ^

Al fine di mantenere questa risposta up-to-date, vale la pena ricordare che la maggior parte della discussione di questa pagina è ora discutibile ora con C # 7.1 e .NET 4.7 che supporta una sintassi sottile che produce anche il miglior codice iL.

esempio originale del PO ...

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 trovato che uno uso comune per la nuova sintassi è quando si sta scrivendo un .NET tipo di valore (cioè struct in C # ) che implementa IEquatable<MyStruct> (come la maggior parte dovrebbero). Dopo aver implementato il metodo Equals(MyStruct other) fortemente tipizzato, è ora possibile reindirizzare con grazia l'override Equals(Object obj) non tipizzata (ereditato da Object) ad esso come segue:

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


Appendice: La generazione Release IL codice per i primi due esempi funzioni sopra indicati in questa risposta (rispettivamente) sono riportati qui. Mentre il codice IL per la nuova sintassi è effettivamente 1 byte più piccolo, vince lo più grande, rendendo lo zero chiamate (contro due) ed evitando l'operazione unbox tutto quando possibile.

// 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 ne dimostrano la mia osservazione circa le prestazioni del nuovo C # 7 la sintassi superando le opzioni precedentemente disponibili, si veda qui (in particolare, ad esempio 'D').

Non ho tempo di provarlo, ma si consiglia di avere:

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

come

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

Si sta creando un nuovo oggetto di volta in volta, che non spiegherà completamente il problema, ma possono contribuire.

Ho provato il tipo esatto controllo costrutto

typeof(int) == item.GetType(), che esegue veloce come la versione item is int, e sempre restituisce il numero (enfasi: anche se hai scritto un Nullable<int> alla matrice, si avrebbe bisogno di utilizzare typeof(int)). È inoltre necessario un controllo aggiuntivo null != item qui.

Tuttavia

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

Il typeof-costrutto è nei miei occhi il modo più veloce per esattamente il controllo di tipo, in quanto utilizza la RuntimeTypeHandle. Dal momento che le tipologie esatte in questo caso non coincidono con annullabile, la mia ipotesi è, is/as hanno a che fare ulteriore heavylifting qui a garantire che in realtà è un esempio di un tipo Nullable.

E onestamente: che cosa fa il tuo is Nullable<xxx> plus HasValue si acquista? Niente. Si può sempre andare direttamente al tipo di sottostante (valore) (in questo caso). O si ottiene il valore o "no, non un'istanza del tipo che stavi chiedendo". Anche se hai scritto (int?)null alla matrice, il controllo di tipo restituisce false.

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

Nota: test precedente è stato fatto all'interno di VS, il debug di configurazione, utilizzando VS2009, utilizzando Core i7 (società di macchina di sviluppo).

Il seguente è stato fatto sulla mia macchina con Core 2 Duo, utilizzando 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
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top