Question

Je suis juste la révision du chapitre 4 de C # en profondeur qui traite des types nullable, et je suis d'ajouter une section sur l'utilisation de l'opérateur « comme », 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 qu'il pourrait améliorer les performances sur C # 1 équivalent, en utilisant « est » suivie d'un casting - après tout, de cette façon, nous ne devons demander type dynamique vérifier une fois, puis une simple vérification de la valeur.

Cela ne semble pas être le cas, cependant. J'ai inclus une application de test exemple ci-dessous, qui résume essentiellement tous les entiers dans un tableau d'objets - mais le tableau contient beaucoup de références nulles et les références de chaîne ainsi que des entiers en boîte. Les mesures de référence le code que vous auriez à utiliser en C # 1, le code en utilisant l'opérateur « comme », et juste pour le plaisir d'une solution LINQ. À mon grand étonnement, le code est 20 fois plus rapide dans ce cas C # 1 -. Et même le code LINQ (que je serais censé être plus lent, compte tenu des itérateurs impliqués) bat le code « comme »

La mise en œuvre .NET de isinst pour les types nullables vraiment lent? Est-ce le unbox.any supplémentaire qui cause le problème? Y at-il une autre explication? En ce moment, on dirait que je vais devoir inclure une mise en garde contre ce dans des situations sensibles aux performances ...

Résultats:

  

Avec: 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);
    }
}
Était-ce utile?

La solution

Il est clair que le code de la machine le compilateur JIT peut générer pour le premier cas est beaucoup plus efficace. Une règle qui aide vraiment il est qu'un objet ne peut être Unboxed à une variable qui a le même type que la valeur boxed. Cela permet au compilateur JIT pour générer un code très efficace, aucune conversion de valeur doivent être pris en considération.

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. Le casting est également facile, le compilateur JIT connaît l'emplacement des bits de valeur dans l'objet et les utilise directement. Pas de copie ou de conversion se produit, tout le code de la machine est en ligne et prend environ une douzaine, mais les instructions. Ce besoin d'être de retour vraiment efficace dans .NET 1.0 quand la boxe était commun.

à int? prend beaucoup plus de travail. La représentation de la valeur de l'entier boîte est pas compatible avec la mise en mémoire de Nullable<int>. Une conversion est nécessaire et le code est difficile en raison de types enum possibles en boîte. Le compilateur JIT génère un appel à une fonction d'assistance CLR nommé JIT_Unbox_Nullable pour faire le travail. Cette fonction est d'usage général pour tout type de valeur, beaucoup de code là-bas pour vérifier les types. Et la valeur est copiée. Difficile d'estimer le coût, car ce code est verrouillé à l'intérieur mscorwks.dll, mais des centaines d'instructions de code machine est probable.

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

Autres conseils

Il me semble que le isinst est vraiment lent sur les types nullable. Dans la méthode FindSumWithCast j'ai changé

if (o is int)

à

if (o is int?)

qui a également ralentit considérablement l'exécution. La seule differenc en IL Je peux voir que

isinst     [mscorlib]System.Int32

obtient changé à

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

Cette origine a commencé comme un commentaire à une excellente réponse de Hans Passant, mais il a trop longtemps donc je veux ajouter quelques morceaux ici:

Tout d'abord, l'opérateur C # as émettra une instruction isinst IL (le fait que l'opérateur de is). (Une autre instruction intéressante est castclass, emited lorsque vous faites une distribution directe et le compilateur sait que le contrôle d'exécution ne peut pas être ommited.)

Voici ce que isinst fait ( ECMA 335 Partition III, 4,6 ):

  

Format: isinst typeTok

     

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

     

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

     

Si typeTok est un type Nullable, Nullable<T>, il est interprété comme « boîte » T

Plus important encore:

  

Si le type réel (pas le vérificateur de type suivi) de obj est verifier cessible à le typeTok type puis isinst réussit et obj (comme résultat ) est retourné inchangé tandis que la vérification suit son type comme typeTok . Contrairement à coercitions (§1.6) et des conversions (§3.27), isinst ne change jamais le type réel d'un objet et préserve l'identité des objets (voir la partition I).

Alors, le tueur de performance ne sont pas isinst dans ce cas, mais le unbox.any supplémentaire. Ce ne montre pas clairement la réponse de Hans, en regardant le code JITed seulement. En général, le compilateur C # émettra un unbox.any après une isinst T? (mais l'omettre dans le cas où vous faites isinst T, quand T est un type de référence).

Pourquoi faut-il faire? isinst T? n'a jamais l'effet qui aurait été évident, à savoir que vous obtenez un retour T?. Au lieu de cela, toutes ces instructions est d'assurer que vous avez un "boxed T" qui peut être Unboxed à T?. Pour obtenir une T? réelle, nous avons encore besoin unbox notre "boxed T" à T?, ce qui explique pourquoi le compilateur émet un unbox.any après isinst. Si vous y pensez, cela est logique parce que le « format boîte » pour T? est juste un "boxed T" et faire castclass et isinst effectuer la Unbox serait incompatible.

Sauvegarde de la conclusion de Hans quelques informations de la norme , ici il va:

(ECMA 335 Partition III, 4,33): unbox.any

  

Lorsqu'il est appliqué à la forme en boîte d'un type de valeur, l'instruction unbox.any extrait la valeur contenue dans obj (de type O). (Il est équivalent à unbox suivie par ldobj). Lorsqu'il est appliqué à un type de référence, l'instruction unbox.any a le même effet que castclass typeTok.

(ECMA 335 Partition III, 4,32): unbox

  

Typiquement, unbox calcule simplement l'adresse du type de valeur qui est déjà présent à l'intérieur de l'objet emballé. Cette approche est impossible lorsque unboxing types de valeur nullables. Parce que les valeurs de Nullable<T> sont converties en Ts en boîte pendant l'opération de la boîte, une mise en œuvre doit souvent fabriquer une nouvelle Nullable<T> sur le tas et calculer l'adresse de l'objet nouvellement allouée.

Fait intéressant, je suis passé sur la rétroaction sur le support par l'intermédiaire de l'opérateur dynamic étant un ordre de grandeur plus lente pour Nullable<T> (similaire à ce test précoce ) - Je soupçonne que, pour des raisons très similaires

.

Gotta love Nullable<T>. Un autre plaisir est que même si les taches JIT (et) Retire null pour les structures non-nullable, il le Borks 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;
    }
}

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

Ceci est le résultat de FindSumWithCast:

Les résultats:

  • Utilisation as, il teste d'abord si un objet est une instance de Int32; sous le capot, il utilise isinst Int32 (qui est similaire à code écrit à la main: if (o est int)). Et en utilisant as, il a également Unbox inconditionnellement l'objet. Et c'est une véritable performance tueur appeler une propriété (il est encore une fonction sous le capot), IL_0027

  • Utilisation de la distribution, vous testez d'abord si l'objet est un int de if (o is int); sous le capot est d'utiliser isinst Int32. Si elle est une instance de int, alors vous pouvez Unbox en toute sécurité la valeur, IL_002D

En termes simples, c'est le pseudo-code de l'utilisation de l'approche as:

int? x;

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

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

Et c'est le pseudo-code en utilisant l'approche de cast:

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

Ainsi, le casting ((int)a[i], bien la syntaxe ressemble à un casting, mais il est en fait unboxing, acteurs et part unboxing la même syntaxe, la prochaine fois que je serai pédant avec la bonne terminologie) approche est vraiment plus rapide, vous ne nécessaire à unbox une valeur lorsqu'un objet est décidément un int. La même chose ne peut pas dire à l'aide d'une approche as.

Le profilage en outre:

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

}

Sortie:

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 peut-on déduire de ces chiffres?

  • approche d'abord, est-alors coulé est nettement plus rapide que comme approche . 303 vs 3524
  • Deuxièmement, .Value est légèrement plus lent que la coulée. 3524 vs 3272
  • Troisièmement, .HasValue est légèrement plus lent que l'utilisation manuelle a (i.e.. Utilisant est ). 3524 vs 3282
  • Quatrièmement, faire une comparaison pomme à la pomme (par exemple à la fois attribution de HasValue simulé et la conversion Valeur simulée se ensemble) entre simulé comme et réel approche, nous peut voir simulé comme est toujours nettement plus rapide que réel . 395 vs 3524
  • Enfin, d'après la première et la quatrième conclusion, il y a quelque chose de mal avec comme la mise en œuvre ^ _ ^

Pour que cette réponse mise à jour, il convient de mentionner que la plupart des discussions sur cette page est maintenant sans objet maintenant avec C # 7.1 et 4.7 .NET qui supporte une syntaxe mince qui produit également le meilleur code IL.

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'un usage commun pour la nouvelle syntaxe est lorsque vous écrivez un type .NET de valeur (c.-à-struct C # ) qui implémente IEquatable<MyStruct> (comme la plupart devrait). Après la mise en œuvre de la méthode de redirection Equals(MyStruct other) fortement typé, vous pouvez maintenant avec grâce le remplacement typées de Equals(Object obj) (héritée de Object) à lui comme suit:

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


Annexe: La construction de Release IL code pour les deux premières fonctions exemple ci-dessus dans cette réponse (respectivement) sont donnés ici. Alors que le code IL pour la nouvelle syntaxe est en effet 1 octet plus petit, il gagne la plupart du temps en faisant grand zéro appels (contre deux) et en évitant l'opération de unbox tout à fait lorsque cela est possible.

// 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 fondent ma remarque sur la performance de la nouvelle C # 7 syntaxe surpassant les options disponibles précédemment, voir ici (en particulier, par exemple 'D').

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

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

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

Vous créez un nouvel objet à chaque fois, ce qui ne va pas expliquer complètement le problème, mais il peut y contribuer.

J'ai essayé la construction de contrôle de type exact

typeof(int) == item.GetType(), qui fonctionne aussi vite que la version item is int et renvoie toujours le nombre (attention: même si vous avez écrit un Nullable<int> au tableau, vous devez utiliser typeof(int)). Vous devez également un chèque de null != item supplémentaire ici.

Cependant

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

La typeof-construction est à mes yeux le moyen le plus rapide pour exactement vérification de type, car il utilise le RuntimeTypeHandle. Étant donné que les types exacts dans ce cas, ne correspondent pas annulable, je suppose, is/as doivent faire heavylifting supplémentaires ici à faire en sorte qu'il est en fait une instance d'un type Nullable.

Et honnêtement: qu'est-ce que votre is Nullable<xxx> plus HasValue vous achetez? Rien. Vous pouvez toujours aller directement au type (valeur) sous-jacente (dans ce cas). Soit vous obtenez la valeur ou « non pas une instance du type que vous demandiez ». Même si vous avez écrit (int?)null au tableau, la vérification de type retournera 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);
    }
}

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]

Note: test précédent a été fait à l'intérieur VS, debug de configuration, en utilisant VS2009, en utilisant 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
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top