337 votes

Performance surprise "avec" et des types nullables

Je suis juste une révision du chapitre 4 de C# en Profondeur qui traite avec des types nullables, et je vais ajouter une section sur l'utilisation de l' "que" de l'opérateur, qui 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'elle pourrait améliorer les performances sur le C# 1 équivalent, à l'aide de "est", suivi par un casting - après tout, de cette façon, nous avons seulement besoin de demander la dynamique de la vérification de type à la fois, et puis une simple vérification de valeur.

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

Est le .NET de la mise en œuvre de l' isinst pour les types nullables vraiment lent? Est-il additionnelles unbox.any que les causes du problème? Est-il une autre explication pour cela? Au moment où il se sent comme je vais avoir à inclure une mise en garde contre l'utilisation de cette performance des situations délicates...

Résultats:

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

212voto

Hans Passant Points 475940

Clairement le code de l'ordinateur, le compilateur JIT peut générer, pour le premier cas est beaucoup plus efficace. Une règle qui aide vraiment il n'y a qu'un objet ne peut être unboxed à une variable qui a le même type que la boîte de la valeur. Qui permet au compilateur JIT pour générer très efficace de code, pas de valeur conversions doivent être considérés.

Le est exploitant de test est facile, il suffit de vérifier si l'objet n'est pas nulle et est du type attendu, mais prend un peu de code machine des instructions. Le casting est également facile, le compilateur JIT connaît l'emplacement de la valeur des bits dans l'objet et utilise directement. Aucune copie ou la conversion se produit, tous les code machine est en ligne et qu'il faut, mais une douzaine d'instructions. Ce devait être vraiment efficace .NET 1.0 lors de la boxe a été commune.

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

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

26voto

0xA3 Points 73439

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

if (o is int)

pour

if (o is int?)

qui a également ralentit considérablement l'exécution. La seule differenc dans IL je peux voir, c'est que

isinst     [mscorlib]System.Int32

est changé à

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

22voto

Johannes Rudolph Points 19845

Cette origine a commencé comme un Commentaire de Hans Passant excellente réponse, mais c'était trop long donc je veux ajouter quelques morceaux ici:

Tout d'abord, le C# en tant qu'opérateur émet un isinst instruction IL (le fait de l'opérateur). (Note de côté: l'autre intéressant d'instruction est castclass, rependu, quand tu fais un direct en fonte et le compilateur sait que le contrôle d'exécution ne peut pas être omis)..

Voici ce qu'il 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 type il est interprété comme la boîte ğ typeTok. Si
  • typeTok est un type nullable, Nullable, il est interprété comme ―boîte‖ T.

Le plus important:

Si le type réel (pas le vérificateur de suivi type) de l'obj est verifier-assignables-le type typeTok puis isinst réussit et obj (comme résultat) est retourné à l'identique, bien que la vérification des pistes de son type comme typeTok. Contrairement aux forçages (§1.6) et les conversions (§3.27), isinst ne change pas le type réel de l'objet et se préserve de l'objet identité (voir la Partition j'ai).

Donc, la performance de tueur n'est pas isinst dans ce cas, mais additionnelles unbox.any. Ce n'était pas clair à partir de Hans Réponse, alors qu'il regardait la JITed code uniquement. En général, le compilateur C# émet un unbox.any après 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 pour effet qu'aurait été évidente, à savoir que vous obtenez en retour un T?. Au lieu de cela, l'ensemble de ces instructions, assurez-vous que vous avez un "boxed T" qui peut être unboxed d' T?. Pour obtenir une réelle T?, nous avons encore besoin de unbox notre "boxed T" de T?, c'est pourquoi le compilateur émet un unbox.après isinst. Si vous pensez à ce sujet, cela fait sens car la zone "format" pour T? est juste un "boxed T" et de faire castclass et isinst effectuer le unbox serait incompatible.

La sauvegarde de Hans trouver des informations à partir de la Norme, ici, il va:

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

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

(ECMA 335 Partition III, 4.32): unbox

[Remarque: en règle générale, unbox simplement calcule l'adresse de la valeur de type qui est déjà présent à l'intérieur de la boîte de l'objet. Cette approche est pas possible lorsque unboxing nullable types de valeur. Parce Que Nullable les valeurs sont converties en boîte Ts au cours de la zone de l'opération, un la mise en œuvre doivent souvent la fabrication d'un nouveau Nullable sur le tas et de calculer l'adresse de la nouvelle objet alloué. la note de fin]

19voto

Marc Gravell Points 482669

Fait intéressant, je suis passé sur les commentaires à propos de soutien aux opérateurs via dynamic étant un ordre de grandeur plus lent pour Nullable<T> (similaire à ce début de test) - je soupçonne, pour des raisons très semblables.

Gotta love Nullable<T>. Un autre plaisir est que même si l'équipe des taches (et supprime) null pour les non nullable les structures, il 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;
    }
}

12voto

Michael Buen Points 20453

C'est le résultat de FindSumWithAsAndHas ci-dessus: alt text

C'est le résultat de FindSumWithCast: alt text

Résultats:

  • À l'aide de as,- il tester d'abord si un objet est une instance de Int32; sous le capot, c'est à l'aide de isinst Int32 (ce qui est similaire à la main le code écrit par: si (o est de type int) ). Et à l'aide de as, il est également sans condition unbox l'objet. Et c'est une véritable performance-killer à l'appel d'un bien(c'est encore une fonction sous le capot), IL_0027

  • À l'aide de fonte, vous vérifiez d'abord si l'objet est une int if (o is int); sous le capot, c'est à l'aide de isinst Int32. Si il est une instance de type int, alors vous pouvez en toute sécurité unbox la valeur, IL_002D

Simplement, c'est le pseudo-code de l'aide d' as approche:

int? x;

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

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

Et c'est le pseudo-code de l'utilisation de la fonte d'approche:

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

De sorte que le cast ((int)a[i], eh bien, la syntaxe ressemble à un casting, mais c'est en fait l'unboxing, la distribution et l'unboxing de partager la même syntaxe, la prochaine fois, je vais être pédant avec le bouton droit de la terminologie), dont l'approche est vraiment plus rapide, vous avez seulement besoin de unbox une valeur lorsqu'un objet est décidément une int. La même chose ne peut pas être dit à l'aide d'un as approche.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X