Surprise de performances avec les types «as» et nullable

330

Je suis en train de réviser le chapitre 4 de C # en profondeur qui traite des types nullables, et j'ajoute une section sur l'utilisation de l'opérateur "as", qui vous permet d'écrire:

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

Je pensais que c'était vraiment bien, et que cela pourrait améliorer les performances par rapport à l'équivalent C # 1, en utilisant "est" suivi d'un cast - après tout, de cette façon, nous n'avons besoin de demander une vérification de type dynamique qu'une seule fois, puis une simple vérification de valeur .

Cependant, cela ne semble pas être le cas. J'ai inclus un exemple d'application de test ci-dessous, qui résume essentiellement tous les entiers dans un tableau d'objets - mais le tableau contient de nombreuses références nulles et des références de chaîne ainsi que des entiers encadrés. Le benchmark mesure le code que vous auriez à utiliser en C # 1, le code utilisant l'opérateur "as", et juste pour lancer une solution LINQ. À mon grand étonnement, le code C # 1 est 20 fois plus rapide dans ce cas - et même le code LINQ (qui je m'attendais à être plus lent, étant donné les itérateurs impliqués) bat le code "as".

L'implémentation .NET des isinsttypes nullables est-elle vraiment très lente? Est-ce le supplément unbox.anyqui cause le problème? Y a-t-il une autre explication à cela? Pour le moment, je pense que je vais devoir inclure un avertissement contre l'utilisation de ceci dans des situations sensibles aux performances ...

Résultats:

Distribution: 10000000: 121
Comme: 10000000: 2211
LINQ: 10000000: 2143

Code:

using System;
using System.Diagnostics;
using System.Linq;

class Test
{
    const int Size = 30000000;

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

        FindSumWithCast(values);
        FindSumWithAs(values);
        FindSumWithLinq(values);
    }

    static void FindSumWithCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            if (o is int)
            {
                int x = (int) o;
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Cast: {0} : {1}", sum, 
                          (long) sw.ElapsedMilliseconds);
    }

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

    static void FindSumWithLinq(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = values.OfType<int>().Sum();
        sw.Stop();
        Console.WriteLine("LINQ: {0} : {1}", sum, 
                          (long) sw.ElapsedMilliseconds);
    }
}
Jon Skeet
la source
8
Pourquoi ne pas regarder le code jitted? Même le débogueur VS peut le montrer.
Anton Tykhyy
2
Je suis juste curieux, avez-vous également testé avec CLR 4.0?
Dirk Vollmar
1
@Anton: Bon point. Fera à un moment donné (bien que ce ne soit pas en VS pour le moment :) @divo: Oui, et c'est pire tout au long. Mais c'est en version bêta, donc il peut y avoir beaucoup de code de débogage là-dedans.
Jon Skeet
1
Aujourd'hui, j'ai appris que vous pouvez utiliser des astypes nullables. Intéressant, car il ne peut pas être utilisé sur d'autres types de valeur. En fait, plus surprenant.
leppie
3
@Lepp, il est parfaitement logique qu'il ne fonctionne pas sur les types de valeur. Pensez-y, asessayez de transtyper en un type et s'il échoue, il retourne null. Vous ne pouvez pas définir les types de valeurs sur null
Earlz

Réponses:

209

Il est clair que le code machine que le compilateur JIT peut générer pour le premier cas est beaucoup plus efficace. Une règle qui aide vraiment est qu'un objet ne peut être décompressé que vers une variable qui a le même type que la valeur encadrée. Cela permet au compilateur JIT de générer du code très efficace, aucune conversion de valeur ne doit être prise en compte.

Le est opérateur de test est facile, il suffit de vérifier si l'objet est non nul et est du type attendu, ne prend que quelques instructions de code machine. La conversion est également facile, le compilateur JIT connaît l'emplacement des bits de valeur dans l'objet et les utilise directement. Aucune copie ou conversion ne se produit, tout le code machine est en ligne et ne prend qu'une douzaine d'instructions. Cela devait être vraiment efficace dans .NET 1.0 lorsque la boxe était courante.

Casting à int? demande beaucoup plus de travail. La représentation des valeurs de l'entier encadré n'est pas compatible avec la disposition de la mémoire de Nullable<int>. Une conversion est requise et le code est délicat en raison des types d'énumération encadrés possibles. Le compilateur JIT génère un appel à une fonction d'assistance CLR nommée JIT_Unbox_Nullable pour effectuer le travail. Il s'agit d'une fonction à usage général pour tout type de valeur, beaucoup de code pour vérifier les types. Et la valeur est copiée. Difficile d'estimer le coût car ce code est enfermé dans mscorwks.dll, mais des centaines d'instructions de code machine sont probables.

La méthode d'extension Linq OfType () utilise également l' opérateur is et le transtypage. Il s'agit cependant d'une conversion vers un type générique. Le compilateur JIT génère un appel à une fonction d'assistance, JIT_Unbox () qui peut effectuer une conversion vers un type de valeur arbitraire. Je n'ai pas une grande explication pourquoi c'est aussi lent que le casting Nullable<int>, étant donné que moins de travail devrait être nécessaire. Je soupçonne que ngen.exe pourrait causer des problèmes ici.

Hans Passant
la source
16
D'accord, je suis convaincu. Je suppose que je suis habitué à penser que "est" potentiellement coûteux en raison des possibilités de remonter dans une hiérarchie d'héritage - mais dans le cas d'un type de valeur, il n'y a aucune possibilité de hiérarchie, donc cela peut être une simple comparaison au niveau du bit . Je pense toujours que le code JIT pour le cas nullable pourrait être optimisé par le JIT beaucoup plus lourd qu'il ne l'est cependant.
Jon Skeet
26

Il me semble que le isinstest vraiment très lent sur les types nullables. Dans la méthode FindSumWithCastj'ai changé

if (o is int)

à

if (o is int?)

ce qui ralentit également considérablement l'exécution. La seule différence en IL que je peux voir est que

isinst     [mscorlib]System.Int32

est changé en

isinst     valuetype [mscorlib]System.Nullable`1<int32>
Dirk Vollmar
la source
1
C'est plus que ça; dans le cas "cast", le isinstest suivi d'un test de nullité puis conditionnellement d' un unbox.any. Dans le cas annulable, il y a un inconditionnel unbox.any .
Jon Skeet
Oui, se révèle à la fois isinst et unbox.anysont plus lents sur les types nullable.
Dirk Vollmar
@ Jon: Vous pouvez revoir ma réponse pour savoir pourquoi le casting est nécessaire. (Je sais que c'est vieux, mais je viens de découvrir ce q et j'ai pensé que je devrais fournir mon 2c de ce que je sais sur le CLR).
Johannes Rudolph
22

Cela a commencé à l'origine comme un commentaire à l'excellente réponse de Hans Passant, mais il est devenu trop long, donc je veux ajouter quelques bits ici:

Tout d'abord, l' asopérateur C # émettra une isinstinstruction IL (tout comme l' isopérateur). (Une autre instruction intéressante est castclassémise lorsque vous effectuez une conversion directe et que le compilateur sait que la vérification de l'exécution ne peut pas être omise.)

Voici ce que isinstfait ( ECMA 335 Partition III, 4.6 ):

Format: isinst typeTok

typeTok est un jeton de métadonnées (a typeref, typedefou typespec), indiquant la classe souhaitée.

Si typeTok est un type de valeur non nullable ou un type de paramètre générique, il est interprété comme typeTok «en boîte» .

Si typeTok est un type nullableNullable<T> , il est interprété comme «encadré»T

Plus important encore:

Si le type réel (et non le type suivi par le vérificateur) d' obj est attribuable au vérificateur au type typeTok, cela isinstréussit et obj (comme résultat ) est renvoyé tel quel tandis que la vérification suit son type en tant que typeTok . Contrairement aux coercitions (§1.6) et aux conversions (§3.27), isinstne change jamais le type réel d'un objet et préserve l'identité de l'objet (voir Partition I).

Donc, le tueur de performances n'est pas isinstdans ce cas, mais en plus unbox.any. Cela n'était pas clair dans la réponse de Hans, car il ne regardait que le code JITed. En général, le compilateur C # émettra un unbox.anyafter a isinst T?(mais l'omettra au cas où vous le feriez isinst T, quand Test un type de référence).

Pourquoi ça fait ça? isinst T?n'a jamais l'effet qui aurait été évident, c'est-à-dire que vous récupérez a T?. Au lieu de cela, toutes ces instructions garantissent que vous en avez un "boxed T"qui peut être déballé T?. Pour obtenir une réelle T?, nous avons encore besoin de notre unbox "boxed T"à T?, ce qui explique pourquoi le compilateur émet un unbox.anyaprès isinst. Si vous y réfléchissez, cela a du sens car le "format de boîte" pour T?est juste un "boxed T"et la création castclasset l' isinstexécution de la boîte seraient incohérentes.

Sauvegardant la découverte de Hans avec quelques informations de la norme , voici:

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

Lorsqu'elle est appliquée à la forme encadrée d'un type de valeur, l' unbox.anyinstruction extrait la valeur contenue dans obj (de type O). (Il équivaut à unboxsuivi de ldobj.) Lorsqu'elle est appliquée à un type de référence, l' unbox.anyinstruction a le même effet que castclasstypeTok.

(ECMA 335 Partition III, 4.32): unbox

En règle générale, unboxcalcule simplement l'adresse du type de valeur qui est déjà présente à l'intérieur de l'objet encadré. Cette approche n'est pas possible lors du déballage des types de valeurs nullables. Étant donné que les Nullable<T>valeurs sont converties en boxed Tspendant l'opération de box, une implémentation doit souvent fabriquer un nouveau Nullable<T>sur le tas et calculer l'adresse de l'objet nouvellement alloué.

Johannes Rudolph
la source
Je pense que la dernière phrase citée pourrait avoir une faute de frappe; "... sur le tas ..." ne devrait-il pas être "sur la pile d'exécution ?" Il semble que le déballage dans une nouvelle instance de tas GC permute le problème d'origine contre un nouveau presque identique.
Glenn Slayden
19

Fait intéressant, j'ai transmis des commentaires sur le soutien de l'opérateur en dynamicétant plus lent d'un ordre de grandeur Nullable<T>(similaire à ce premier test ) - je soupçonne pour des raisons très similaires.

Je dois aimer Nullable<T>. Un autre plaisir est que même si le JIT repère (et supprime) nullpour les structures non nullables, il le fausse pour 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
la source
Yowser. C'est une différence vraiment douloureuse. Eek.
Jon Skeet
Si aucun autre bien n'est sorti de tout cela, cela m'a amené à inclure des avertissements pour mon code d'origine et celui-ci :)
Jon Skeet
Je sais que c'est une vieille question, mais pourriez-vous expliquer ce que vous entendez par "les taches JIT (et supprime) nullpour les structures non nullables"? Voulez-vous dire qu'il remplace nullpar une valeur par défaut ou quelque chose pendant l'exécution?
Justin Morgan
2
@Justin - une méthode générique peut être utilisée au moment de l'exécution avec un nombre illimité de permutations de paramètres génériques ( Tetc.). Les exigences de la pile, etc. dépendent des arguments (quantité d'espace de pile pour un local, etc.), vous obtenez donc un JIT pour toute permutation unique impliquant un type de valeur. Cependant, les références sont toutes de la même taille, alors partagez un JIT. Tout en effectuant le JIT de type par valeur, il peut vérifier quelques scénarios évidents et essaie de supprimer le code inaccessible en raison de choses comme des valeurs nulles impossibles. Ce n'est pas parfait, notez. En outre, j'ignore AOT pour ce qui précède.
Marc Gravell
Le test annulable sans restriction est toujours plus lent de 2,5 ordres de grandeur, mais une optimisation est en cours lorsque vous n'utilisez pas la countvariable. L'ajout Console.Write(count.ToString()+" ");après le watch.Stop();dans les deux cas ralentit les autres tests d'un peu moins d'un ordre de grandeur, mais le test annulable sans restriction n'est pas modifié. Notez qu'il y a également des changements lorsque vous testez les cas lorsqu'il nullest passé, confirmant que le code d'origine ne fait pas vraiment la vérification et l'incrémentation nulles pour les autres tests. Linqpad
Mark Hurd
12

Ceci est le résultat de FindSumWithAsAndHas ci-dessus: texte alternatif

Ceci est le résultat de FindSumWithCast: texte alternatif

Résultats:

  • À l'aide as, il teste d'abord si un objet est une instance de Int32; sous le capot qu'il utilise isinst Int32(ce qui est similaire au code manuscrit: si (o est int)). Et en utilisant as, il déballe également inconditionnellement l'objet. Et c'est un vrai tueur de performances d'appeler une propriété (c'est toujours une fonction sous le capot), IL_0027

  • En utilisant cast, vous testez d'abord si l'objet est un int if (o is int); sous le capot que cela utilise isinst Int32. S'il s'agit d'une instance de int, vous pouvez décompresser la valeur en toute sécurité, IL_002D

Autrement dit, voici le pseudo-code d'utilisation de l' asapproche:

int? x;

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

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

Et voici le pseudo-code d'utilisation de l'approche Cast:

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

Donc, le cast ( (int)a[i], bien que la syntaxe ressemble à un cast, mais c'est en fait unboxing, cast et unboxing partagent la même syntaxe, la prochaine fois que je serai pédant avec la bonne terminologie) l'approche est vraiment plus rapide, il vous suffit de déballer une valeur quand un objet est décidément un int. On ne peut pas dire la même chose en utilisant une asapproche.

Michael Buen
la source
11

Afin de maintenir cette réponse à jour, il convient de mentionner que la plupart des discussions sur cette page sont désormais sans objet avec C # 7.1 et .NET 4.7 qui prend en charge une syntaxe mince qui produit également le meilleur code IL.

L'exemple original de l'OP ...

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

devient simplement ...

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

J'ai trouvé qu'une utilisation courante de la nouvelle syntaxe est lorsque vous écrivez un type de valeur .NET (c'est- structà- dire en C # ) qui implémente IEquatable<MyStruct>(comme la plupart le devraient). Après avoir implémenté la Equals(MyStruct other)méthode fortement typée , vous pouvez désormais rediriger avec élégance le Equals(Object obj)remplacement non typé (hérité de Object) comme suit:

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

 


Annexe: Le code IL deRelease construction pour les deux premiers exemples de fonctions montrés ci-dessus dans cette réponse (respectivement) est donné ici. Bien que le code IL pour la nouvelle syntaxe soit en effet 1 octet plus petit, il gagne généralement gros en faisant zéro appel (contre deux) et en évitant complètement l' opération lorsque cela est possible.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

Pour d'autres tests qui corroborent ma remarque sur les performances de la nouvelle syntaxe C # 7 dépassant les options précédemment disponibles, voir ici (en particulier, l'exemple «D»).

Glenn Slayden
la source
9

Profilage supplémentaire:

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

}

Production:

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

Que pouvons-nous déduire de ces chiffres?

  • Tout d' abord, est-then-cast approche est nettement plus rapide que comme approche. 303 vs 3524
  • Deuxièmement, la valeur est légèrement plus lente que la coulée. 3524 vs 3272
  • Troisièmement, .HasValue est légèrement plus lent que l'utilisation du manuel (c'est-à-dire que l'utilisation est ). 3524 vs 3282
  • Quatrièmement, en faisant une comparaison de pomme à pomme (c'est-à-dire que l'assignation de HasValue simulée et la conversion de valeur simulée se produisent ensemble) entre l' approche simulée et réelle comme approche, nous pouvons voir que la simulation est encore beaucoup plus rapide que la réalité . 395 vs 3524
  • Enfin, d' après la première et la quatrième conclusion, il y a quelque chose de mal avec comme mise en œuvre ^ _ ^
Michael Buen
la source
8

Je n'ai pas le temps de l'essayer, mais vous voudrez peut-être avoir:

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

comme

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

Vous créez un nouvel objet à chaque fois, ce qui n'expliquera pas complètement le problème, mais peut y contribuer.

James Black
la source
1
Non, j'ai couru ça et c'est un peu plus lent.
Henk Holterman
2
Déclarer une variable à un endroit différent n'affecte le code généré que de manière significative lorsque la variable est capturée (à quel point cela affecte la sémantique réelle) dans mon expérience. Notez qu'il ne crée pas un nouvel objet sur le tas, bien qu'il crée certainement une nouvelle instance de int?sur la pile à l'aide unbox.any. Je soupçonne que c'est le problème - je suppose que l'IL fabriqué à la main pourrait battre les deux options ici ... bien qu'il soit également possible que le JIT soit optimisé pour reconnaître le cas is / cast et ne vérifier qu'une seule fois.
Jon Skeet
Je pensais que le casting est probablement optimisé car il existe depuis si longtemps.
James Black
1
is / cast est une cible facile pour l'optimisation, c'est un idiome tellement ennuyeux.
Anton Tykhyy
4
Les variables locales sont allouées sur la pile lorsque le cadre de pile pour la méthode est créé, donc où vous déclarez la variable dans la méthode ne fait aucune différence. (À moins que ce ne soit bien sûr une fermeture, mais ce n'est pas le cas ici.)
Guffa
8

J'ai essayé la construction de vérification de type exacte

typeof(int) == item.GetType(), qui fonctionne aussi vite que la item is intversion et renvoie toujours le nombre (accentuation: même si vous avez écrit un Nullable<int>dans le tableau, vous devrez utiliser typeof(int)). Vous avez également besoin d'un null != itemcontrôle supplémentaire ici.

toutefois

typeof(int?) == item.GetType()reste rapide (contrairement à item is int?), mais renvoie toujours false.

Le typeof-construct est à mes yeux le moyen le plus rapide pour la vérification de type exacte , car il utilise le RuntimeTypeHandle. Étant donné que les types exacts dans ce cas ne correspondent pas à nullable, je suppose que vous is/asdevez effectuer un levage supplémentaire ici pour vous assurer qu'il s'agit bien d'une instance de type Nullable.

Et honnêtement: qu'est-ce que vous is Nullable<xxx> plus HasValuevous achetez? Rien. Vous pouvez toujours accéder directement au type (valeur) sous-jacent (dans ce cas). Vous obtenez la valeur ou "non, pas une instance du type que vous demandiez". Même si vous avez écrit (int?)nulldans le tableau, la vérification de type renverra false.

dalo
la source
Intéressant ... l'idée d'utiliser le "as" + HasValue (not is plus HasValue, note) est qu'il n'effectue la vérification de type qu'une fois au lieu de deux. Il fait le "check and unbox" en une seule étape. Cela se ressent comme il devrait être plus rapide ... mais il est clairement pas. Je ne sais pas ce que vous entendez par la dernière phrase, mais il n'y a rien de tel qu'un encadré int?- si vous encadrez une int?valeur, elle se termine par un int encadré ou une nullréférence.
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);
    }
}

Les sorties:

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]

Remarque: Le test précédent a été effectué à l'intérieur de VS, débogage de la configuration, à l'aide de VS2009, à l'aide de Core i7 (machine de développement de l'entreprise).

Ce qui suit a été fait sur ma machine en utilisant Core 2 Duo, en utilisant 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
la source
Quelle version du framework utilisez-vous, par intérêt? Les résultats sur mon netbook (en utilisant .NET 4RC) sont encore plus spectaculaires - les versions utilisant As sont bien pires que vos résultats. Peut-être l'ont-ils amélioré pour .NET 4 RTM? Je pense toujours que ça pourrait être plus rapide ...
Jon Skeet
@Michael: exécutiez-vous une version non optimisée ou exécutiez-vous dans le débogueur?
Jon Skeet
@Jon: build non optimisé, sous débogueur
Michael Buen
1
@Michael: Droite - J'ai tendance à voir les résultats de performance sous un débogueur comme largement non pertinents :)
Jon Skeet
@Jon: If by under debugger, signifiant à l'intérieur de VS; oui le benchmark précédent a été fait sous le débogueur. J'ai à nouveau testé, à l'intérieur et à l'extérieur de VS, et compilé comme débogage et compilé comme version. Vérifiez l'édition
Michael Buen