C # 'est' les performances de l'opérateur

102

J'ai un programme qui nécessite des performances rapides. Dans l'une de ses boucles internes, je dois tester le type d'un objet pour voir s'il hérite d'une certaine interface.

Une façon de procéder serait d'utiliser la fonctionnalité de vérification de type intégrée du CLR. La méthode la plus élégante est probablement le mot-clé 'is':

if (obj is ISpecialType)

Une autre approche serait de donner à la classe de base ma propre fonction virtuelle GetType () qui retourne une valeur d'énumération prédéfinie (dans mon cas, en fait, je n'ai besoin que d'un booléen). Cette méthode serait rapide, mais moins élégante.

J'ai entendu dire qu'il existe une instruction IL spécifiquement pour le mot clé «est», mais cela ne signifie pas qu'il s'exécute rapidement lorsqu'il est traduit en assemblage natif. Quelqu'un peut-il partager un aperçu de la performance de «est» par rapport à l'autre méthode?

MISE À JOUR: Merci pour toutes les réponses éclairées! Il semble que quelques points utiles soient répartis entre les réponses: le point d'Andrew sur le fait de «exécuter» automatiquement un casting est essentiel, mais les données de performance recueillies par Binary Worrier et Ian sont également extrêmement utiles. Ce serait formidable si l'une des réponses était modifiée pour inclure toutes ces informations.

JubJub
la source
2
btw, CLR ne vous donnera pas la possibilité de créer votre propre fonction Type GetType (), car elle enfreint l'une des principales règles CLR - vraiment des types
abatishchev
1
Euh, je ne suis pas tout à fait sûr de ce que vous entendez par la règle "vraiment types", mais je comprends que le CLR a une fonction Type GetType () intégrée. Si je devais utiliser cette méthode, ce serait avec une fonction d'un nom différent renvoyant une énumération, donc il n'y aurait pas de conflit de nom / symbole.
JubJub
3
Je pense qu'abatishchev voulait dire «sécurité de type». GetType () n'est pas virtuel pour empêcher un type de mentir sur lui-même et donc de préserver la sécurité du type.
Andrew Hare
2
Avez-vous envisagé de pré-extraire et de mettre en cache la conformité de type afin de ne pas avoir à le faire dans des boucles? Il semble que chaque question de performance soit toujours massivement +1, mais cela me semble être une mauvaise compréhension de c #. Est-ce vraiment trop lent? Comment? Qu'avez-vous essayé? Evidemment pas grand chose compte tenu de vos commentaires sur les réponses ...
Gusdor

Réponses:

114

L'utilisation ispeut nuire aux performances si, une fois que vous avez vérifié le type, vous effectuez un cast vers ce type. islance réellement l'objet dans le type que vous vérifiez, de sorte que toute conversion ultérieure est redondante.

Si vous envisagez de lancer de toute façon, voici une meilleure approche:

ISpecialType t = obj as ISpecialType;

if (t != null)
{
    // use t here
}
Andrew Hare
la source
1
Merci. Mais si je ne vais pas lancer l'objet si le conditionnel échoue, est-ce que je ferais mieux d'utiliser une fonction virtuelle pour tester le type à la place?
JubJub
4
@JubJub: non. Un échec aseffectue essentiellement la même opération que is(à savoir, la vérification de type). La seule différence est qu'il revient ensuite nullau lieu de false.
Konrad Rudolph
74

Je suis avec Ian , tu ne veux probablement pas faire ça.

Cependant, pour que vous le sachiez, il y a très peu de différence entre les deux, plus de 10000000 itérations

  • Le contrôle d'énumération arrive à 700 millisecondes (environ)
  • Le contrôle IS arrive à 1000 millisecondes (environ)

Personnellement, je ne résoudrais pas ce problème de cette façon, mais si j'étais obligé de choisir une méthode, ce serait le contrôle intégré du SI, la différence de performance ne vaut pas la peine de prendre en compte la surcharge de codage.

Mes classes de base et dérivées

class MyBaseClass
{
    public enum ClassTypeEnum { A, B }
    public ClassTypeEnum ClassType { get; protected set; }
}

class MyClassA : MyBaseClass
{
    public MyClassA()
    {
        ClassType = MyBaseClass.ClassTypeEnum.A;
    }
}
class MyClassB : MyBaseClass
{
    public MyClassB()
    {
        ClassType = MyBaseClass.ClassTypeEnum.B;
    }
}

JubJub: Comme demandé plus d'informations sur les tests.

J'ai exécuté les deux tests à partir d'une application console (une version de débogage) chaque test ressemble à ce qui suit

static void IsTest()
{
    DateTime start = DateTime.Now;
    for (int i = 0; i < 10000000; i++)
    {
        MyBaseClass a;
        if (i % 2 == 0)
            a = new MyClassA();
        else
            a = new MyClassB();
        bool b = a is MyClassB;
    }
    DateTime end = DateTime.Now;
    Console.WriteLine("Is test {0} miliseconds", (end - start).TotalMilliseconds);
}

En cours de sortie, j'obtiens une différence de 60 à 70 ms, comme Ian.

Mise à jour supplémentaire - 25 octobre 2012
Après quelques années, j'ai remarqué quelque chose à ce sujet, le compilateur peut choisir d'omettre bool b = a is MyClassBdans la version car b n'est utilisé nulle part.

Ce code. . .

public static void IsTest()
{
    long total = 0;
    var a = new MyClassA();
    var b = new MyClassB();
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < 10000000; i++)
    {
        MyBaseClass baseRef;
        if (i % 2 == 0)
            baseRef = a;//new MyClassA();
        else
            baseRef = b;// new MyClassB();
        //bool bo = baseRef is MyClassB;
        bool bo = baseRef.ClassType == MyBaseClass.ClassTypeEnum.B;
        if (bo) total += 1;
    }
    sw.Stop();
    Console.WriteLine("Is test {0} miliseconds {1}", sw.ElapsedMilliseconds, total);
}

. . . montre systématiquement le iscontrôle arrivant à environ 57 millisecondes et la comparaison d'énumération à 29 millisecondes.

NB je préfère quand même le ischèque, la différence est trop petite pour m'en soucier

Binaire Worrier
la source
35
+1 pour tester réellement les performances, au lieu de supposer.
Jon Tackabury
3
Il est préférable de faire un test avec la classe Chronomètre, au lieu de DateTime.Maintenant, ce qui est très cher
abatishchev
2
Je vais prendre cela en compte, mais dans ce cas, je ne pense pas que cela affecterait le résultat. Merci :)
Binary Worrier
11
@Binary Worrier- Vos nouvelles allocations d'opérateurs de classes vont complètement éclipser toutes les différences de performances dans les opérations «est». Pourquoi ne supprimez-vous pas ces nouvelles opérations en réutilisant deux instances pré-allouées différentes, puis réexécutez le code et publiez vos résultats.
1
@mcmillab: je vous garantis que quoi que vous fassiez, vous aurez des goulots d'étranglement de plusieurs ordres de grandeur plus grands que toute dégradation des performances que l' isopérateur vous cause, et que le surentendu de la conception et du codage autour de l' isopérateur coûtera une fortune en la qualité du code et sera finalement autodestructrice en termes de performances. Dans ce cas, je maintiens ma déclaration. L'opérateur «est» ne sera jamais le problème avec vos performances d'exécution.
Binary Worrier
23

Ok, donc j'ai discuté de cela avec quelqu'un et j'ai décidé de tester cela davantage. Autant que je sache, les performances asetis sont toutes deux très bonnes, comparées au test de votre propre membre ou fonction pour stocker des informations de type.

J'ai utilisé Stopwatch, ce que je viens d'apprendre n'est peut-être pas l'approche la plus fiable, alors j'ai également essayé UtcNow. Plus tard, j'ai également essayé l'approche du temps du processeur qui semble similaire à l' UtcNowinclusion de temps de création imprévisibles. J'ai également essayé de rendre la classe de base non abstraite sans virtuels, mais cela ne semblait pas avoir d'effet significatif.

J'ai couru cela sur un Quad Q6600 avec 16 Go de RAM. Même avec des itérations de 50 mil, les chiffres rebondissent toujours autour de +/- 50 millisecondes, donc je ne lirais pas trop les différences mineures.

Il était intéressant de voir que x64 créé plus rapidement mais exécuté comme / est plus lent que x86

Mode de sortie x64:
Chronomètre:
As: 561 ms
Est: 597 ms
Propriété de base: 539 ms
Champ de base: 555 ms
Champ RO de base: 552
ms Test virtuel GetEnumType (): 556
ms Test Virtual IsB (): 588 ms
Temps de création: 10416 ms

UtcNow:
As: 499ms
Est: 532ms
Propriété de base: 479ms
Champ de base: 502ms
Champ RO de base: 491ms
Virtual GetEnumType (): 502ms
Virtual bool IsB (): 522ms
Temps de création: 285ms (Ce nombre semble peu fiable avec UtcNow. J'obtiens également 109ms et 806ms.)

x86 Release Mode:
Chronomètre:
As: 391ms
Est: 423ms
Propriété de base: 369ms
Champ de base: 321ms
Champ RO de base: 339ms Test
virtuel GetEnumType (): 361ms Test
Virtual IsB (): 365ms
Temps de création: 14106ms

UtcNow:
As: 348ms
Est: 375ms
Propriété de base: 329ms
Champ de base: 286ms
Champ RO de base: 309ms
Virtual GetEnumType (): 321ms
Virtual bool IsB (): 332ms
Temps de création: 544ms (Ce nombre semble peu fiable avec UtcNow.)

Voici l'essentiel du code:

    static readonly int iterations = 50000000;
    void IsTest()
    {
        Process.GetCurrentProcess().ProcessorAffinity = (IntPtr)1;
        MyBaseClass[] bases = new MyBaseClass[iterations];
        bool[] results1 = new bool[iterations];

        Stopwatch createTime = new Stopwatch();
        createTime.Start();
        DateTime createStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            if (i % 2 == 0) bases[i] = new MyClassA();
            else bases[i] = new MyClassB();
        }
        DateTime createStop = DateTime.UtcNow;
        createTime.Stop();


        Stopwatch isTimer = new Stopwatch();
        isTimer.Start();
        DateTime isStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] =  bases[i] is MyClassB;
        }
        DateTime isStop = DateTime.UtcNow; 
        isTimer.Stop();
        CheckResults(ref  results1);

        Stopwatch asTimer = new Stopwatch();
        asTimer.Start();
        DateTime asStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i] as MyClassB != null;
        }
        DateTime asStop = DateTime.UtcNow; 
        asTimer.Stop();
        CheckResults(ref  results1);

        Stopwatch baseMemberTime = new Stopwatch();
        baseMemberTime.Start();
        DateTime baseStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].ClassType == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime baseStop = DateTime.UtcNow;
        baseMemberTime.Stop();
        CheckResults(ref  results1);

        Stopwatch baseFieldTime = new Stopwatch();
        baseFieldTime.Start();
        DateTime baseFieldStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].ClassTypeField == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime baseFieldStop = DateTime.UtcNow;
        baseFieldTime.Stop();
        CheckResults(ref  results1);


        Stopwatch baseROFieldTime = new Stopwatch();
        baseROFieldTime.Start();
        DateTime baseROFieldStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].ClassTypeField == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime baseROFieldStop = DateTime.UtcNow;
        baseROFieldTime.Stop();
        CheckResults(ref  results1);

        Stopwatch virtMethTime = new Stopwatch();
        virtMethTime.Start();
        DateTime virtStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].GetClassType() == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime virtStop = DateTime.UtcNow;
        virtMethTime.Stop();
        CheckResults(ref  results1);

        Stopwatch virtMethBoolTime = new Stopwatch();
        virtMethBoolTime.Start();
        DateTime virtBoolStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].IsB();
        }
        DateTime virtBoolStop = DateTime.UtcNow;
        virtMethBoolTime.Stop();
        CheckResults(ref  results1);


        asdf.Text +=
        "Stopwatch: " + Environment.NewLine 
          +   "As:  " + asTimer.ElapsedMilliseconds + "ms" + Environment.NewLine
           +"Is:  " + isTimer.ElapsedMilliseconds + "ms" + Environment.NewLine
           + "Base property:  " + baseMemberTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Base field:  " + baseFieldTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Base RO field:  " + baseROFieldTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Virtual GetEnumType() test:  " + virtMethTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Virtual IsB() test:  " + virtMethBoolTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Create Time :  " + createTime.ElapsedMilliseconds + "ms" + Environment.NewLine + Environment.NewLine+"UtcNow: " + Environment.NewLine + "As:  " + (asStop - asStart).Milliseconds + "ms" + Environment.NewLine + "Is:  " + (isStop - isStart).Milliseconds + "ms" + Environment.NewLine + "Base property:  " + (baseStop - baseStart).Milliseconds + "ms" + Environment.NewLine + "Base field:  " + (baseFieldStop - baseFieldStart).Milliseconds + "ms" + Environment.NewLine + "Base RO field:  " + (baseROFieldStop - baseROFieldStart).Milliseconds + "ms" + Environment.NewLine + "Virtual GetEnumType():  " + (virtStop - virtStart).Milliseconds + "ms" + Environment.NewLine + "Virtual bool IsB():  " + (virtBoolStop - virtBoolStart).Milliseconds + "ms" + Environment.NewLine + "Create Time :  " + (createStop-createStart).Milliseconds + "ms" + Environment.NewLine;
    }
}

abstract class MyBaseClass
{
    public enum ClassTypeEnum { A, B }
    public ClassTypeEnum ClassType { get; protected set; }
    public ClassTypeEnum ClassTypeField;
    public readonly ClassTypeEnum ClassTypeReadonlyField;
    public abstract ClassTypeEnum GetClassType();
    public abstract bool IsB();
    protected MyBaseClass(ClassTypeEnum kind)
    {
        ClassTypeReadonlyField = kind;
    }
}

class MyClassA : MyBaseClass
{
    public override bool IsB() { return false; }
    public override ClassTypeEnum GetClassType() { return ClassTypeEnum.A; }
    public MyClassA() : base(MyBaseClass.ClassTypeEnum.A)
    {
        ClassType = MyBaseClass.ClassTypeEnum.A;
        ClassTypeField = MyBaseClass.ClassTypeEnum.A;            
    }
}
class MyClassB : MyBaseClass
{
    public override bool IsB() { return true; }
    public override ClassTypeEnum GetClassType() { return ClassTypeEnum.B; }
    public MyClassB() : base(MyBaseClass.ClassTypeEnum.B)
    {
        ClassType = MyBaseClass.ClassTypeEnum.B;
        ClassTypeField = MyBaseClass.ClassTypeEnum.B;
    }
}
Jared Thirsk
la source
45
(Quelques bonus 5h - inspiré de Shakespeare ...) Etre, ou ne pas être: telle est la question: Est-ce qu'il est plus noble dans le code de souffrir Les énumérations et propriétés des bases abstraites, Ou bien reprendre les offres d'un intermédiaire linguiste Et en invoquant son instruction, leur faire confiance? Deviner: se demander; Pas plus; et par un timing pour discerner, nous mettons fin au mal de tête et aux mille émerveillements subconscients dont les codeurs temporels sont héritiers. C'est une fermeture à souhaiter sincèrement. Mourir, non, mais dormir; Oui je dormirai, peut-être pour rêver de l'est et comme dans ce qui peut être dérivé de la plus basique de la classe.
Jared Thirsk
Peut-on en conclure que l'accès à une propriété est plus rapide sur x64 que l'accès à un champ !!! Parce que c'est une sacrée surprise pour moi comment cela peut être?
Didier A.
1
Je ne conclurais pas que, parce que: "Même avec des itérations de 50 mil, les chiffres rebondissent toujours autour de +/- 50 millisecondes, donc je ne lirais pas trop les différences mineures."
Jared Thirsk
16

Andrew a raison. En fait, avec l'analyse du code, cela est signalé par Visual Studio comme une distribution inutile.

Une idée (sans savoir ce que vous faites est un peu une photo dans le noir), mais on m'a toujours conseillé d'éviter de vérifier comme ça, et d'avoir à la place une autre classe. Alors plutôt que de faire quelques vérifications et d'avoir des actions différentes selon le type, faites savoir à la classe comment se traiter ...

par exemple, Obj peut être ISpecialType ou IType;

les deux ont une méthode DoStuff () définie. Pour IType, il peut simplement renvoyer ou effectuer des tâches personnalisées, tandis que ISpecialType peut faire d'autres choses.

Cela supprime alors complètement tout casting, rend le code plus propre et plus facile à maintenir, et la classe sait comment faire ses propres tâches.

Ian
la source
Oui, puisque tout ce que je vais faire si les tests de type sont vrais, c'est appeler une certaine méthode d'interface, je pourrais simplement déplacer cette méthode d'interface dans la classe de base et ne rien faire par défaut. Cela peut être plus élégant que de créer une fonction virtuelle pour tester le type.
JubJub
J'ai fait un test similaire à Binary Worrier après les commentaires d'abatishchev et n'ai trouvé que 60 ms de différence sur 10 000 000 d'itérations.
Ian
1
Wow, merci pour l'aide. Je suppose que je vais m'en tenir à l'utilisation des opérateurs de vérification de type pour le moment, à moins qu'il ne semble approprié de réorganiser la structure de classe. J'utiliserai l'opérateur 'as' comme Andrew l'a suggéré car je ne veux pas lancer de redondance.
JubJub
15

J'ai fait une comparaison des performances sur deux possibilités de comparaison de types

  1. myobject.GetType () == typeof (MaClasse)
  2. myobject est MyClass

Le résultat est: l'utilisation de "is" est environ 10 fois plus rapide !!!

Production:

Heure de la comparaison de types: 00: 00: 00.456

Heure de la comparaison Is: 00: 00: 00.042

Mon code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace ConsoleApplication3
{
    class MyClass
    {
        double foo = 1.23;
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyClass myobj = new MyClass();
            int n = 10000000;

            Stopwatch sw = Stopwatch.StartNew();

            for (int i = 0; i < n; i++)
            {
                bool b = myobj.GetType() == typeof(MyClass);
            }

            sw.Stop();
            Console.WriteLine("Time for Type-Comparison: " + GetElapsedString(sw));

            sw = Stopwatch.StartNew();

            for (int i = 0; i < n; i++)
            {
                bool b = myobj is MyClass;
            }

            sw.Stop();
            Console.WriteLine("Time for Is-Comparison: " + GetElapsedString(sw));
        }

        public static string GetElapsedString(Stopwatch sw)
        {
            TimeSpan ts = sw.Elapsed;
            return String.Format("{0:00}:{1:00}:{2:00}.{3:000}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds);
        }
    }
}
Knasterbax
la source
13

Andrew Hare a fait remarquer que les performances isétaient perdues lorsque vous effectuez une vérification, puis que la conversion était valide, mais en C # 7.0, nous pouvons faire est de vérifier la correspondance du modèle de sorcière pour éviter une distribution supplémentaire plus tard:

if (obj is ISpecialType st)
{
   //st is in scope here and can be used
}

En outre, si vous avez besoin de vérifier entre plusieurs types, les constructions de correspondance de modèles C # 7.0 vous permettent désormais de faire switchsur les types:

public static double ComputeAreaModernSwitch(object shape)
{
    switch (shape)
    {
        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Rectangle r:
            return r.Height * r.Length;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

Vous pouvez en savoir plus sur la correspondance de modèles en C # dans la documentation ici .

Krzysztof Branicki
la source
1
Une solution valide, bien sûr, mais cette fonctionnalité de correspondance de modèles C # me rend triste, quand elle encourage un code «d'envie de fonctionnalités» comme celui-ci. Sûrement nous devrions nous efforcer d'encapsuler la logique où seuls les objets dérivés "savent" calculer leur propre aire, et ensuite ils renvoient simplement la valeur?
Dib
2
Le SO a besoin de boutons de filtre (sur la question) pour les réponses qui s'appliquent aux nouvelles versions d'un framework, d'une plate-forme, etc. Cette réponse forme la base de la bonne pour C # 7.
Nick Westgate
1
Les idéaux @Dib OOP sont éjectés de la fenêtre lorsque vous travaillez avec des types / classes / interfaces que vous ne contrôlez pas. Cette approche est également utile lors de la gestion du résultat d'une fonction qui peut renvoyer une valeur parmi plusieurs de types complètement différents (car C # ne prend toujours pas en charge les types d'union - vous pouvez utiliser des bibliothèques comme OneOf<T...>mais elles ont des inconvénients majeurs) .
Dai
4

Au cas où quelqu'un se demanderait, j'ai effectué des tests dans le moteur Unity 2017.1, avec la version d'exécution de script .NET4.6 (Experimantal) sur un ordinateur portable avec un processeur i5-4200U. Résultats:

Average Relative To Local Call LocalCall 117.33 1.00 is 241.67 2.06 Enum 139.33 1.19 VCall 294.33 2.51 GetType 276.00 2.35

Article complet: http://www.ennoble-studios.com/tuts/unity-c-performance-comparison-is-vs-enum-vs-virtual-call.html

Gru
la source
Le lien de l'article est mort.
James Wilkins
Lien @James relancé.
Gru
Bon truc - mais je ne vous ai pas voté contre (en fait, j'ai voté de toute façon); Au cas où vous vous poseriez la question. :)
James Wilkins
-3

On m'a toujours conseillé d'éviter de vérifier comme ça et d'avoir à la place une autre classe. Alors plutôt que de faire quelques vérifications et d'avoir des actions différentes selon le type, faites savoir à la classe comment se traiter ...

par exemple, Obj peut être ISpecialType ou IType;

les deux ont une méthode DoStuff () définie. Pour IType, il peut simplement renvoyer ou effectuer des tâches personnalisées, tandis que ISpecialType peut faire d'autres choses.

Cela supprime alors complètement tout casting, rend le code plus propre et plus facile à maintenir, et la classe sait comment faire ses propres tâches.

user3802787
la source
1
Cela ne répond pas à la question. Quoi qu'il en soit, les classes peuvent ne pas toujours savoir comment se traiter en raison d'un manque de contexte. Nous appliquons une logique similaire à la gestion des exceptions lorsque nous autorisons les exceptions à remonter la chaîne d'appels jusqu'à ce qu'une méthode / fonction ait suffisamment de contexte pour gérer les erreurs.
Vakhtang