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.

4 commentaires »

  1. Salut,

    J’ai le sentiment qu’il y a un problème avec la manière d’appeler la méthode en mode Reflexion. Cela peut expliquer l’incohérence. Pourrais-tu fournir le code complet du test.

    Commentaire par descombes — 27/07/2010 @ 10:29

    • Je n’ai pas retrouvé le code source de cet exemple.

      Mais pour l’appel de méthode par reflection, j’utilise la méthode décrite dans l’article suivante : https://sebastiencourtois.wordpress.com/2010/04/16/nouveauts-c-net-4-introduction-au-c-dynamique/ (méthode CallMethod).

      Commentaire par sebastiencourtois — 27/07/2010 @ 10:37

      • C’est bien ce que je craignais. En pratique, on optimiserait. Comme c’est une boucle, on sortirait l’appel à GetMethod qui est couteux. Le vrai coût porterait alors sur l’Invoke, ce qui changerait totalement les résultats.

        Commentaire par descombes — 27/07/2010 @ 16:12

  2. J’ai fait un test complet en incluant l’optimisation (en sortant le GetMethode de la boucle). J’obtiens toujours des résultats meilleur pour dynamic par rapport à reflection.
    Pour 1 000 000 d’appels
    Direct : 00:00:00.0072613
    Reflec : 00:00:01.8779117
    Reflec Optim : 00:00:01.3252002
    Dynamic : 00:00:00.0764506

    Commentaire par sebastiencourtois — 28/07/2010 @ 16:25


RSS feed for comments on this post. TrackBack URI

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

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

%d blogueurs aiment cette page :