Astuces DotNet (Sébastien Courtois)

03/05/2010

[.NET] Immutabilité et chaines de caractères en .NET

Filed under: .NET, C#, Débutant, Hors Catégorie, Intermediaire, Optimisation — Étiquettes : , , , , , , , — sebastiencourtois @ 15:31

Nous avons vu brièvement lors d’un post précédent un objet “immutable” (en anglais, immuable en vrai français) : Le Tuple”. Toutefois, nous ne sommes pas rentrés dans les détails de ce type d’objet et nous n’avons pas étudié le plus utilisé de tous : La classe String.

  • Définition de l’immutabilité

Un objet “immutable” est une objet dont les propriétés sont définies à la création de l’objet puis ne peuvent plus changer durant la vie de l’objet.

Une des raisons de la création de ce type d’objet (c’est d’ailleurs aussi un design pattern) : la gestion de la mémoire. En effet, vu que les données sont créées à la création de l’objet, il suffit d’allouer l’ensemble de la mémoire d’un coup et de ne plus y toucher jusqu’à la destruction de l’objet.

Un autre avantages est le multithreading. En effet, si l’objet n’évolue pas au cours de sa vie, tous les threads peuvent y accéder quand ils veulent sans avoir besoin de se synchroniser. Cela permet une programmation plus simple et un programme plus rapide.

  • Exemple concret d’immutabilité : String

Un objet “immutable” que nous utilisons tous les jours sans le savoir spécialement : la classe string.

Prenons l’exemple C/C++ suivant :

#include <stdio.h>
#include <string.h>

int main(int argc,char **argv)
{
    char *test = new char[7];
    strcpy(test,"Bonjour");
    printf("%c",test[2]);
    test[2] = 'N';
    printf("%c",test[2]);
}

Le programme copie une chaine (“bonjour”) dans un tableau de caractère puis affiche le 3ème caractères (‘n’) puis décide de modifier ce troisième caractères et de le réafficher. Tout cela se passe normalement, on obtient “BoNjour” à la fin de l’éxécution du programme.

Prenons le même type de programme en C# avec une string.

static void Main(string[] args)
{
    string test = "Bonjour le monde !!!";
    Console.WriteLine(test[2]);
    test[2] = 'N';
    Console.WriteLine(test[2]);
}

Rien ne laisse présager un problème, on utilise System.String comme une tableau de caractère et on modifie sa troisième valeur. Et pourtant on obtient l’erreur suivante :

immutable1

Un petit tour sur le code de la classe System.String et on voit en effet que l’indexer est uniquement en GET :

// Summary:
//     Represents text as a series of Unicode characters.
[Serializable]
[ComVisible(true)]
…….
    // Summary:
    //     Gets the character at a specified character position in the current System.String
    //     object.
    //
    // Parameters:
    //   index:
    //     A character position in the current string.
    //
    // Returns:
    //     A Unicode character.
    //
    // Exceptions:
    //   System.IndexOutOfRangeException:
    //     index is greater than or equal to the length of this object or less than
    //     zero.
    public char this[int index] { get; }

On peut donc voir que la classe String est bien immutable car sa valeur est fixé à sa création et après la chaine devient read only.

  • Les méthodes de la classe System.String

Vous pourriez vous dire, à raison, que la classe string contient des méthodes permettant modifier la chaine après création (ToUpper/ToLower). Or si vous remarquez bien, l’ensemble de ces méthodes retournent un type string. La chaine en paramètre est la chaine d’entrée et restera toujours à la même valeur alors que la valeur de retour sera une nouvelle chaine traité à partir de la première.

Un petit exemple tiré de la MSDN démontrant cela :

class Program
{
  static void Main(string[] args)
  {
     string testString = "A TEST STRING"; Console.WriteLine("testString: " + testString + Environment.NewLine);
     Console.WriteLine("Performing 'testString.ToLower();'");
     // Does not alter the string. Returns a new lowercase string, but           
     // we do not assign it to anything, so it is discarded            
    testString.ToLower();
    Console.WriteLine("testString: " + testString + Environment.NewLine);
    Console.WriteLine("Performing 'string lowerString = " + "testString.ToLower();'");
    // Assign the returned string to a new variable           
    string lowerString = testString.ToLower();
    Console.WriteLine("lowerString: {0}{1}testString: {2}{1}",
    lowerString, Environment.NewLine, testString);
    // Finally convert the test string to lowercase by assigning the             
    // result of ToLower back to itself. Note that this still doesn't            
    // result in the original testString object changing; instead, the            
    // old testString is discarded and the variable is set to the newly-            
    // created lowercase string object           
    Console.WriteLine("Performing 'testString = testString.ToLower();'");
    testString = testString.ToLower();
    Console.WriteLine("testString: " + testString);
    Console.ReadLine();
  }
}

  • Cas de la concaténation de chaines

L’utilisation du caractères ‘+’ pour la concaténation de chaines s’est énormément répandu depuis la sortie du .NET. Toutefois, il faut savoir que cela peut être désastreux pour les performance. En effet, chaque utilisation de ‘+’ entraine la création d’une nouvelle chaine.  Ainsi l’ajout de 1000 caractères un à un avec le caractère ‘+’ va entrainer la création de 1000 chaines de caractères allant de 0 à 999 caractères (999! caractères pour les matheux).

Dans ce cas, on préconise l’utilisation de la version “mutable” (comprendre : modifiable) de System.String : StringBuilder. On peut voir l’intérêt dans le test suivant :

static void Main(string[] args)
{
    for (int nb = 1; nb < 1000000; nb *= 10)
    {
        Stopwatch sw = Stopwatch.StartNew();
        string s = "";
        for (int i = 0; i < nb; i++)
            s += "T";
        sw.Stop();
        Console.WriteLine("Temps string \t {0} \t: {1}", nb, sw.Elapsed);

        Stopwatch sw2 = Stopwatch.StartNew();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < nb; i++)
            sb.Append("T");
        sw2.Stop();
        Console.WriteLine("Temps SB \t {0} \t: {1}", nb, sw2.Elapsed);
    }
    Console.ReadLine();
}

Résultat du test de performance :

Nombre Concaténation Temps String Temps StringBuilder
1,00 00:00:00.0000015 00:00:00.0000034
10,00 00:00:00.0000076 00:00:00.0000007
100,00 00:00:00.0000138 00:00:00.0000026
1 000,00 00:00:00.0011658 00:00:00.0000211
10 000,00 00:00:00.0388231 00:00:00.0001182
100 000,00 00:00:05.4556546 00:00:00.0011412

Le résultat est sans appel en faveur de StringBuilder qui est jusqu’à 500x plus rapide que la concaténation avec le caractère +.

  • Conclusion

Il faut donc faire attention lorsque l’on utilise des objets du framework afin de savoir s’ils sont “mutables” ou non car, bien qu’un objet “immutable” soit là pour permettre des meilleurs performances, cela peut s’avérer le contraire lorsqu’il est mal utilisé (exemple string VS StringBuilder).

Publicités

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.

28/01/2010

[.NET] Le mot clé sealed sur une classe permet-il permettre de gagner en performance ?

Filed under: .NET, Débutant, Intermediaire, Optimisation — Étiquettes : , , , , , — sebastiencourtois @ 00:19

Voici la question que Wilfried Woivre (MSP ainsi que “Ask The Expert” sur Azure aux prochains Tech Days 2010).

Ma première réponse a été… “J’en sais rien” :). En effet, le mot clé sealed a toujours été, pour moi, utilisé pour des design OO spécifiques (type création de framework public …) où l’idée est d’interdire l’extension d’une classe (la fonction première du mot clé sealed). J’ai donc dit que je pensais que ce n’était pas le cas ni le rôle de ce mot clé.

Quelque temps plus tard, je me retrouve à lire un chapitre sur l’optimisation de code .NET dans lequel le mot clé “sealed” == performance. J’ai donc décidé de faire le benchmark suivant :

   public class ClassA
   {
       public void Method1() { }
   }

   public sealed class ClassASealed
   {
       public void Method1() { }
   }

   public class ClassAVirtual
   {
       public virtual void Method1() { }
   }

    
class Program
{
    static void Main(string[] args)
    {
        ClassA ca = new ClassA();
        ClassASealed cas = new ClassASealed();
        ClassAVirtual cav = new ClassAVirtual();

        Stopwatch swca = new Stopwatch();
        Stopwatch swcas = new Stopwatch();
        Stopwatch swcav = new Stopwatch();
        for (int i = 0; i < 10000000; i++)
        {
            swca.Start();
            ca.Method1();
            swca.Stop();

            swcas.Start();
            cas.Method1();
            swcas.Stop();

            swcav.Start();
            cav.Method1();
            swcav.Stop();
        }

        Console.WriteLine("ClassA  : " + swca.Elapsed);
        Console.WriteLine("ClassAS : " + swcas.Elapsed);
        Console.WriteLine("ClassAV : " + swcav.Elapsed);
        Console.WriteLine("Finish");
        Console.ReadLine();
    }
}

On teste, ici, le temps d’appel des méthodes en faisant 1 000 000 d’appels. On trouve les résultats suivants :

  • Classe Normale : 22.8844652 secondes
  • Classe Sealed : 22.8523417 secondes
  • Classe Normale avec une méthode virtuelle : 23.0062174 secondes

Premier constat : Le mot clé sealed nous permet de gagner environ 1 pour 1000 en performance.

Deuxième constat : la méthode virtuelle semble faire perdre aussi des performances ( 5 ou 6 pour 1000).

Pourquoi ces écarts ?

1°)Parlons tous d’abord de l’écart normale/sealed : Au niveau du code IL, il n’y a que le sealed de différence :

.class public auto ansi beforefieldinit ClassAVirtual
.class public auto ansi sealed beforefieldinit ClassASealed

L’optimisation à lieu au niveau du Runtime. En effet, sachant que la classe est scellé, le runtime peut faire un appel direct à la méthode (dans certains cas, exécuter le code sans faire d’appel).

2°) Pour la différence méthode classique /méthode virtuelle :

.method public hidebysig instance void Method1() cil managed
.method public hidebysig newslot virtual instance void Method1() cil managed

Le virtual ici oblige le runtime a créer une table d’appel pour cette méthode virtuelle dans laquelle seront contenu les méthodes des classes dérivées qui surchargeront cette méthode. Par conséquent, un appel à cette méthode est un appel ‘virtuel’ de méthode qui doit aller chercher dans cette table pour trouver la bonne méthode à appelé. Même si dans notre cas, la table ne contient qu’un seul élément, il y a tout de même un coût supplémentaire par rapport à un appel classique (accès à la table,recherche dans la table …)

Conclusion

Effectivement, le fait de mettre rendre classe ‘sealed’ permet de gagner des performances. Toutefois les performances sont tellement faible que dans 95 % des cas (disons : les applications non temps réel ou ne travaillant pas avec des énormes volumes de données), ces gains sont négligeables et il sera plus judicieux de voir s’il n’y a pas des optimisations à faire au niveau du code ou de l’architecture du projet.

Propulsé par WordPress.com.

%d blogueurs aiment cette page :