Astuces DotNet (Sébastien Courtois)

19/04/2010

[Nouveautés .NET 4] Dynamic VS Réflexion VS Code classique : Performances

Filed under: .NET, C# 4, Débutant, Intermediaire, Optimisation — Étiquettes : , , , , , , — sebastiencourtois @ 10:28

Suite à des discussions sur le sujet des performances avec les Dynamics, j’ai décidé de monter le petit test suivant :

class Program
   {
       static void Main(string[] args)
       {
           Test t = new Test();
           dynamic d = t;
           //Premier appel
           Console.WriteLine("Premier appel");
           
           //Appel direct
           Stopwatch sw = Stopwatch.StartNew();
           t.TotoNoParam();
           sw.Stop();
           Console.WriteLine("Appel direct : {0}",sw.Elapsed);
           //Appel reflexion
           sw.Reset();
           sw.Start();
           Test.CallMethod<Test>(t, "TotoNoParam", null);
           sw.Stop();
           Console.WriteLine("Appel Reflection : {0}", sw.Elapsed);
           //Appel dynamique
           sw.Reset();
           sw.Start();
           d.TotoNoParam();
           sw.Stop();
           Console.WriteLine("Appel Dynamique : {0}", sw.Elapsed);

           //Second appel
           Console.WriteLine("Second appel");

           //Appel direct
           sw.Reset();
           sw.Start();
           t.TotoNoParam();
           sw.Stop();
           Console.WriteLine("Appel direct : {0}", sw.Elapsed);
           //Appel reflexion
           sw.Reset();
           sw.Start();
           Test.CallMethod<Test>(t, "TotoNoParam", null);
           sw.Stop();
           Console.WriteLine("Appel Reflection : {0}", sw.Elapsed);
           //Appel dynamique
           sw.Reset();
           sw.Start();
           d.TotoNoParam();
           sw.Stop();
           Console.WriteLine("Appel Dynamique : {0}", sw.Elapsed);

           //Appel chainés
           //Appel direct
           for (int i = 1; i < 10000000; i *= 10)
           {
               sw.Reset();
               sw.Start();
               for (int j = 0; j < i; j++)
                   t.TotoNoParam();
               sw.Stop();
               Console.WriteLine("Appel direct x {0} \t: {1}",i, sw.Elapsed);
           }

           //Appel reflection
           for (int i = 1; i < 10000000; i *= 10)
           {
               sw.Reset();
               sw.Start();
               for (int j = 0; j < i; j++)
                   Test.CallMethod<Test>(t, "TotoNoParam", null);
               sw.Stop();
               Console.WriteLine("Appel reflection x {0} \t: {1}", i, sw.Elapsed);
           }

           //Appel dynamic
           for (int i = 1; i < 10000000; i *= 10)
           {
               sw.Reset();
               sw.Start();
               for (int j = 0; j < i; j++)
                   d.TotoNoParam();
               sw.Stop();
               Console.WriteLine("Appel dynamic x {0} \t: {1}", i, sw.Elapsed);
           }

           Console.WriteLine("Fini");
           Console.ReadLine();
       }

   }

La classe Test est la même que le poste précédent. J’ai d’abord voulu tester les performances pour un appel d’une méthode sans paramètres avec 3 méthodes :

  • Appel direct (classique depuis .NET 1)
  • Appel en utilisant la réflexion (MethodInfo.Invoke)
  • le mot clé dynamic.

Voici les tableaux de résultats ( les graphiques ne rendant pas correctement les valeurs, j’ai choisi de ne pas en mettre).

Sur un appel (en ms) Direct Réflexion Dynamics
Premier appel 0,0622 0,4020 26,0880
Second appel 0,0007 0,0076 0,7007
  88x 53x 37x

Premier constat, un deuxième appel est toujours plus rapide que le premier quelque soit la méthode. Cela est dû à une optimisation de .NET qui compile le code à la volée et le met en cache. Le premier appel subit donc la compilation + mise en cache. Cela ajoute un temps (non négligeable) supplémentaire pour l’exécution de la méthode.

Deuxième constant, on remarque que Direct est 6 à 10 x plus rapide que la réflexion et 400 à 1000x fois plus rapide que Dynamics.

Si l’on regarde les apples multiples, on obtient le tableau suivant (en secondes) :

Appels Multiples Direct Réflexion Dynamics Dynamics/Direct Réflexion / Direct Réflexion/Dynamics
1,00 0,0000003 0,0000069 0,0007376 2 458,67 23,00 0,01
10,00 0,0000003 0,0000238 0,0000015 5,00 79,33 15,87
100,00 0,0000011 0,0002062 0,0000072 6,55 187,45 28,64
1 000,00 0,0000103 0,0020013 0,0000691 6,71 194,30 28,96
10 000,00 0,0000979 0,0211307 0,0006662 6,80 215,84 31,72
100 000,00 0,0009323 0,1582175 0,0067310 7,22 169,71 23,51
1 000 000,00 0,0096497 1,5509957 0,0872035 9,04 160,73 17,79

Remarque : Afin de clarifier les choses, ces tableaux a été généré plusieurs fois afin d’être sur de la validité des résultats.

On peut voir que l’appel classque reste, de loin, la méthode la plus rapide. Direct reste environ 5 à 10 x plus rapide que dynamic et 80 à 200 x plus rapide que la réflexion. On remarque aussi que la reflexion est plus lente (15 à 30x) que Dynamic (ce qui est en contradiction avec le premier tableau). De plus, le premier appel multiple pour Dynamic est étonnamment élevé car il intervient APRES deux appels à cette méthode issus de l’expérience précédente donc il ne devrait plus y avoir de compilation/mise en cache.

Je n’ai pas encore trouvé les raisons de ces “incohérences”. Si vous avez des explications, n’hésitez pas à commenter ce post.

Concernant les appels de méthodes avec paramètres, on se retrouve avec les mêmes plages de valeurs (Direct 5 à 10x plus rapide que reflexion et 500 à 1200 x plus rapide que dynamic). On retrouve les mêmes incohérences du deuxième tableau.

Conclusion :

L’utilisation du mot clé dynamic peut avoir des conséquences dramatiques sur les performances d’un code C# et ne doit donc être utilisé que dans les cas vraiment spécifiques comme l’interopérabilité avec un langages dynamique.

EDIT : 28 / 07 / 2010 : Suite à un commentaire, je publie ici le code ainsi qu’une optimisation proposé pour le test :

class Program
    {
        static void Main(string[] args)
        {
            Test t = new Test();
            dynamic d = t;

            Console.WriteLine("Test Appel Direct");
            Stopwatch sw = new Stopwatch();
            for (int i = 1; i < 10000000; i *= 10)
            {
                sw.Reset();
                sw.Start();
                for (int j = 0; j < i; j++)
                    t.TotoNoParam();
                sw.Stop();
                Console.WriteLine("Appel direct x {0} \t: {1}", i, sw.Elapsed);
            }

            Console.WriteLine("Test Appel Reflection");
            for (int i = 1; i < 10000000; i *= 10)
            {
                sw.Reset();
                sw.Start();
                for (int j = 0; j < i; j++)
                    Program.CallMethod<Test>(t, "TotoNoParam");
                sw.Stop();
                Console.WriteLine("Appel Reflection x {0} \t: {1}", i, sw.Elapsed);
            }

            Console.WriteLine("Test Appel Reflection (optimized)");
            Type type = t.GetType();
            MethodInfo mi = type.GetMethod("TotoNoParam");
            for (int i = 1; i < 10000000; i *= 10)
            {
                sw.Reset();
                sw.Start();
                for (int j = 0; j < i; j++)
                    mi.Invoke(t,null);
                sw.Stop();
                Console.WriteLine("Appel Reflection (optimized) x {0} \t: {1}", i, sw.Elapsed);
            }

            Console.WriteLine("Test Appel Dynamic");
            for (int i = 1; i < 10000000; i *= 10)
            {
                sw.Reset();
                sw.Start();
                for (int j = 0; j < i; j++)
                    d.TotoNoParam();
                sw.Stop();
                Console.WriteLine("Appel Dynamic x {0} \t: {1}", i, sw.Elapsed);
            }
            Console.ReadLine();
        }

        public static void CallMethod<T>(T instance, string methodName) where T : class
        {
            Type t = instance.GetType();
            MethodInfo mi = t.GetMethod("TotoNoParam");
            if (mi == null)
                throw new InvalidOperationException(string.Format("La méthode {0} n'existe pas pour le type {1}", methodName, t.Name));
            mi.Invoke(instance, new object[] { });
        }
    }

    public class Test
    {
        public string ValA { get; set; }
        public int ValB;
        public dynamic ValC;

        public void TotoNoParam() { }
        public void Toto(int a) { }
        public string Tata(dynamic b) { return string.Empty; }
    }

Les résultats :

Test Appel Direct

Appel direct x 1        : 00:00:00.0000614

Appel direct x 10       : 00:00:00.0000003

Appel direct x 100      : 00:00:00.0000011

Appel direct x 1000     : 00:00:00.0000076

Appel direct x 10000    : 00:00:00.0000702

Appel direct x 100000   : 00:00:00.0007034

Appel direct x 1000000  : 00:00:00.0072613

Test Appel Reflection

Appel Reflection x 1    : 00:00:00.0005199

Appel Reflection x 10   : 00:00:00.0000188

Appel Reflection x 100  : 00:00:00.0001620

Appel Reflection x 1000         : 00:00:00.0016719

Appel Reflection x 10000        : 00:00:00.0155864

Appel Reflection x 100000       : 00:00:00.1943837

Appel Reflection x 1000000      : 00:00:01.8779117

Test Appel Reflection (optimized)

Appel Reflection (optimized) x 1        : 00:00:00.0000069

Appel Reflection (optimized) x 10       : 00:00:00.0000184

Appel Reflection (optimized) x 100      : 00:00:00.0001509

Appel Reflection (optimized) x 1000     : 00:00:00.0014998

Appel Reflection (optimized) x 10000    : 00:00:00.0151252

Appel Reflection (optimized) x 100000   : 00:00:00.1431094

Appel Reflection (optimized) x 1000000  : 00:00:01.3252002

Test Appel Dynamic

Appel Dynamic x 1       : 00:00:00.0346741

Appel Dynamic x 10      : 00:00:00.0000053

Appel Dynamic x 100     : 00:00:00.0000107

Appel Dynamic x 1000    : 00:00:00.0000921

Appel Dynamic x 10000   : 00:00:00.0009150

Appel Dynamic x 100000  : 00:00:00.0088054

Appel Dynamic x 1000000         : 00:00:00.0764506

Apparement même en sortant le MethodInfo de l’appel, on obitent encore des résultats plus rapide avec Dynamic qu’avec Reflection.

Publicités

16/04/2010

[Nouveautés C# .NET 4] Introduction au C# dynamique

Filed under: .NET, C# 4, Débutant — Étiquettes : , , , , , , — sebastiencourtois @ 09:42

Une des nouveautés qui a fait couler beaucoup d’encre est l’ajout des types dynamiques à .NET 4. Cela venait d’un besoin pour des langages “dynamiques” tel que le Javascript, Python ou encore le Ruby de pouvoir être facilement interopérable avec les langages plus traditionnels comme le C# ou le VB. Microsoft a ainsi rajouter une couche au framework : La DLR (Dynamic Langage Runtime) ainsi qu’un langage supplémentaire tirant entièrement parti de la dlr : le F#.

  • Le principe de fonctionnement

Le but des types dynamiques de pouvoir appeler des méthodes/propriétés d’un objet et que la résolution (le lien entre le code appelant et le code appelé) ne se fasse pas au moment de la compilation mais que cette résolution se fasse à l’exécution.

dlr Comme on peut le voir ci dessus, les types .NET sont résolus au moyen d’un “object binder” qui n’est autre qu’un code utilisant la réflexion. Pour les langages dynamiques, chacun a sa propre méthode de résolution.

Il est à noter quel la DLR s’appuie sur la CLR. Les langages C#/VB peuvent ainsi choisir d’utiliser le couple DLR/CLR ou uniquement la CLR (comme avant). Lorsque la DLR est utilisé, celle ci retraduit les appels pour qu’il soit exécutable par la CLR.

dlr2

  • Le mot clé dynamic en C#

Afin de décider le type de runtime nous allons utiliser, C# 4 introduit un nouveau mot clé dynamic. Ce mot clé permet d’indiquer que l’instance de l’objet sera interprété par la DLR et non pas par la CLR directement. Cela permet d’appeler n’importe quel méthode/propriété/indexer de la classe sans connaitre son nom et ses paramètres à la compilation.

int a = 0;
dynamic b = a;
b = 1;
//a = 0 , b = 1
List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5 };
dynamic listDynamic = list;
listDynamic.Add(6);
list.Add(7);
//list = {0,1,2,3,4,5,6,7}
//listDynamic = {0,1,2,3,4,5,6,7}

Ce code montre qu’il est possible de convertir tout type (valeur / références) dans des types dynamiques.

dlr3 Le fonctionnement général de la mémoire n’est pas bouleversé par le mot clé dynamique (les types références vont toujours sur le tas, les types valeurs sur la pile). La seule différence réside au niveau de l’object binder qui,dans le cas de types dynamiques, fera la résolution au moment de l’éxécution du programme.

public class Test
{
    public string ValA { get; set; }
    public int ValB;
    public dynamic ValC;

    public void Toto(int a){}
    public string Tata(dynamic b) { return string.Empty; }
}

Concernant le mot clé dynamic, il est aussi possible de l’utiliser dans les définitions des propriétés,les paramètres de méthodes, les types retours, les indexeurs … bref, on peut l’utiliser dans tous les cas.

  • On pouvait faire du dynamique en C# 3.5 ?

Il était possible de réaliser ce type d’opération en .NET 3.5. Voici un exemple de code permettant l’appel de méthode dont on passe le nom en paramètres.

public void CallMethod<T>(T instance, string methodName) where T : class
{
   Type t = instance.GetType();
   MethodInfo mi = t.GetMethod(methodName);
   if (mi == null)
     throw new InvalidOperationException(string.Format("La méthode {0} n'existe pas pour le type {1}", methodName, t.Name));
   mi.Invoke(instance,new object[]{});
}

On remarque que le code reste très complexe pour juste appeler une méthode d’une classe. Microsoft nous a donc maché le travail afin de ne plus à avoir utiliser la reflection dans ce genre de cas.

  • Un grand pouvoir implique de grandes responsabilités

Comme je l’ai dit au début de l’article, cette nouveautés a fait couler beaucoup d’encre. Je suis plutôt contre cette fonctionnalités pour les raisons suivantes :

  1. Lisibilité du code : A l’instar du mot clé var introduit en .NET 3.5, rendre un langage comme C# en partie dynamique va réduire la lisibilité du code car on ne saura plus le type des différentes variables car de nombreux développeurs vont préférer écrire des var/dynamic partout plutôt que typer correctement leurs instances.
  2. Intellisense : Le mot clé dynamic entraine une résolution à l’exécution. Cela veut dire que, au moment de la compilation, on ne dispose d’aucune informations sur la structure des objets que l’on utilise ce qui rend l’écriture du code complexe (une faute de frappe ou de majuscule et on obtient une erreur …. à l’éxécution.)dlr4
  3. Erreur de résolution : Comme expliqué ci-dessus, les fautes de frappes peuvent arriver lorsque l’on code (surtout sans aide de l’intellisense) et on se retrouve avec des erreurs à l’éxécution. Cela introduit un temps supplémentaire car on obtient les erreurs de résolutions après la compilation/exécution. De plus, pour un peu que l’on ne passe pas dans le code où se trouve l’erreur, on ne la voit jamais et c’est l’utilisateur final qui en ait victime.

dlr5

  • Conclusion

L’ajout de la DLR est une fonctionnalité sympathique pour l’interopérabilité entre langages statiques et dynamiques. Cela devrait décupler les possibilités des langages .NET et surement entrainer de nouveaux pattern de développement. Toutefois, à l’instal du mot clé var, il se peut que cela soit au détriment de la qualité/lisibilité du code.

Si vous avez des commentaires/questions sur le sujet, n’hésitez pas à commenter.

Créez un site Web ou un blog gratuitement sur WordPress.com.

%d blogueurs aiment cette page :