Surprise de Performance avec les types " as " et nullable

Je révise juste 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 chouette, et que cela pourrait améliorer les performances par rapport à L'équivalent C# 1, en utilisant "is" suivi d'un cast - après tout, de cette façon, nous n'avons besoin que de demander une vérification de type dynamique une fois, puis une simple vérification de valeur.

Cela ne semble pas être le cas, cependant. J'ai inclus un échantillon de test app 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 de références de chaînes ainsi que des entiers en boîte. Le benchmark mesure le code que vous devrez 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 (que je m'attendais à être plus lent, compte tenu des itérateurs impliqués) bat le code "as".

Est le L'implémentation. net de isinst pour les types nullable est vraiment lente? Est - ce le unbox.any supplémentaire qui cause le problème? Y a-t-il une autre explication à cela? Pour le moment, il me semble que je vais devoir inclure un avertissement contre l'utilisation de cela dans des situations sensibles aux performances...

Résultats:

Fonte: 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);
    }
}
302
demandé sur Jon Seigel 2009-10-17 23:48:57

10 réponses

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 là est qu'un objet ne peut être déballé qu'à une variable qui a le même type que la valeur en boîte. Cela permet au compilateur JIT de générer du code très efficace, aucune conversion de valeur ne doit être considérée.

Leest le test d'opérateur est facile, il suffit de vérifier si l'objet n'est pas null et est du type attendu, ne prend que quelques codes machine instruction. Le cast 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.

Lancer à int? prend beaucoup plus de travail. La représentation de la valeur de l'entier encadré n'est pas compatible avec la disposition de la mémoire de Nullable<int>. Une conversion est nécessaire et le code est délicat en raison de possibles types d'énumération en boîte. Le compilateur JIT génère un appel à une fonction d'assistance CLR nommée JIT_Unbox_Nullable pour effectuer le travail. C'est une fonction à usage général pour n'importe quel type de valeur, beaucoup de code pour vérifier les types. Et la valeur est copiée. Difficile d'estimer le coût puisque ce code est enfermé dans mscorwks.dll, mais des centaines d'instructions de code machine est probable.

La méthode D'extension Linq OfType() utilise également l'opérateur is et la distribution. Ce est cependant un cast à un type générique. Le compilateur JIT génère un appel à une fonction d'assistance, JIT_Unbox() qui peut effectuer une conversion à un type de valeur arbitraire. Je n'ai pas une bonne explication pourquoi il 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.

195
répondu Hans Passant 2010-06-20 12:06:33

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

if (o is int)

À

if (o is int?)

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

isinst     [mscorlib]System.Int32

Devient

isinst     valuetype [mscorlib]System.Nullable`1<int32>
23
répondu Dirk Vollmar 2009-10-17 20:10:02

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'opérateur C# as émettra une instruction isinst IL (tout comme l'opérateur is). (Une autre instruction intéressante est castclass, émise lorsque vous faites une distribution directe et que le compilateur sait que la vérification de l'exécution ne peut pas être omise.)

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

Format: isinst typeTok

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

Si typeTok est un non nullable type de valeur ou un paramètre générique, elle est interprétée comme une "boîte" typeTok.

Si typeTok est un type nullable, Nullable<T>, il est interprété comme "boxed" T

Le Plus important:

Si le type réel (pas le vérificateur suivi type) de obj est verifier-cessible à le type typeTok alors isinst réussit et obj (comme résultat) est retourné à l'identique, bien que la vérification des pistes son type typeTok. Contrairement aux coercions (§1.6) et aux conversions (§3.27), isinst ne modifie jamais le type réel d'un objet et préserve l'identité de l'objet (voir Partition I).

Donc, le tueur de performance n'est pas isinst dans ce cas, mais le unbox.any supplémentaire. Ce N'était pas clair de la réponse de Hans, comme il a regardé le code JITed seulement. En général, le compilateur C# émet un unbox.any après un isinst T? (mais l'omettre dans le cas où vous n' isinst T lorsque T est un type de référence).

Pourquoi faut-il faire? isinst T? n'a jamais l'effet qui aurait été évident, c'est-à-dire que vous récupérez un T?. Au lieu de cela, toutes ces instructions garantissent que vous avez un "boxed T" qui peut être déballé à T?. Pour obtenir un T? réel, nous devons toujours déballer notre "boxed T" à T?, qui est pourquoi le compilateur émet un unbox.any aprè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 faire castclass et isinst Effectuer l'unbox serait incohérent.

Sauvegarde de 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'instruction unbox.any extrait la valeur contenue dans obj (du type O). (Il est équivalent à unbox suivi de ldobj.) Lorsqu'elle est appliquée à 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 encadré. Cette approche n'est pas possible lors du déballage des types de valeur nullable. Parce que les valeurs Nullable<T> sont converties en Ts en boîte pendant la boîte opération, une implémentation doit souvent fabriquer un nouveau Nullable<T> sur le tas et calculer l'adresse de l'objet nouvellement alloué.

22
répondu Johannes Rudolph 2017-08-18 17:48:19

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

Je dois aimer Nullable<T>. Un autre plaisir est que même si le JIT spots (et supprime) null pour les structures non nullables, il le bore 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;
    }
}
19
répondu Marc Gravell 2009-10-17 21:26:50

Ceci est le résultat de FindSumWithAsAndHas ci-dessus: alt text http://www.freeimagehosting.net/uploads/9e3c0bfb75.png

Ceci est le résultat de FindSumWithCast: alt text http://www.freeimagehosting.net/uploads/ce8a5a3934.png

Résultats:

  • En utilisant as, Il teste d'abord si un objet est une instance de Int32; sous le capot, il utilise isinst Int32 (qui est similaire au code écrit à la main: if (o est int)). Et en utilisant as, il a également unbox inconditionnellement l'objet. Et c'est un vrai tueur de performance 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, cela utilise isinst Int32. Si c'est une instance de int, alors vous pouvez décompresser la valeur, IL_002D

En termes simples, c'est le pseudo-code d'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 d'utilisation de cast approche:

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

Donc, le cast ((int)a[i], bien 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 suffit de déballer une valeur quand un objet est décidément un int. La même chose ne peut pas être dite à l'aide d'une approche as.

12
répondu Michael Buen 2011-07-24 01:14:30

Profilage plus loin:

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 pouvons-nous déduire de ces chiffres?

  • tout d'abord, l'approche is-then-cast est significativement 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ème, .HasValue est légèrement plus lent que l'utilisation de Manual has (c'est-à-dire que l'utilisation de est). 3524 vs 3282
  • Quatrièmement, faire une comparaison apple-to-apple(c'est-à-dire à la fois l'attribution de simulé HasValue et la conversion de la Valeur simulée arrive ensemble) entre simulé et réel que approche, nous pouvons voir simulé, qui est encore nettement plus rapide que réel que. 395 vs 3524
  • enfin, basé sur la première et la quatrième conclusion, il y a quelque chose qui ne va pas avec as mise en œuvre ^_ ^
9
répondu Michael Buen 2010-06-21 14:17:32

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 contribuer.

8
répondu James Black 2009-10-17 19:59:51

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

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

Cependant

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

La typeof-construct est à mes yeux le moyen le plus rapide pour la vérification de type exacte, car elle utilise le Propriétaire du. Puisque les types exacts dans ce cas ne correspondent pas à nullable, je suppose que is/as doit faire un heavylifting Supplémentaire ici pour s'assurer qu'il s'agit en fait d'une instance D'un type Nullable.

Et honnêtement: qu'est-ce que votre is Nullable<xxx> plus HasValue vous acheter? Rien. Vous pouvez toujours aller 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?)null dans le tableau, la vérification de type retournera faux.

8
répondu dalo 2010-06-19 10:01:21
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

[MODIFIER: 2010-06-19]

Remarque: le test précédent a été effectué dans VS, configuration debug, en utilisant Vs2009, en utilisant Core i7 (machine de développement d'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
7
répondu Michael Buen 2010-06-19 10:16:59

Afin de garder cette réponse à jour, il convient de mentionner que la plupart des discussions sur cette page sont maintenant discutables 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 pour la nouvelle syntaxe est lorsque vous écrivez un type de valeur. net (c'est-à-dire struct dans C#) qui implémente IEquatable<MyStruct> (comme la plupart). Après avoir implémenté la méthode Equals(MyStruct other) fortement typée, vous pouvez maintenant rediriger gracieusement la substitution Equals(Object obj) non typée (héritée de Object) comme suit:

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

 


Annexe: Le Release construire IL le code pour les deux premiers exemples de fonctions 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 surtout gros en faisant zéro appel (vs deux) et éviter complètement l'opération unbox 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 corroborent ma remarque sur la performance de la nouvelle syntaxe C#7 dépassant les options précédemment disponibles, voir ici (en particulier, exemple 'D').

7
répondu Glenn Slayden 2018-03-28 00:00:59