Frédéric Mélantois
C# 3.0 Beta, déclarations et initialisations simplifiées, regardons sous le capot !
Puisque le produit est encore en beta, portons un regard sur quelques nouveautés du langage et ce qui peut être généré par le compilateur c#.
Par Frédéric Mélantois publié le 25/06/2007 à 00:52, lu 4727 fois, 8 pages
 5 | Initialisation de structures
Une question que l'on peut aussi se poser est la suivante : est-il possible, outre les objets, d'initialiser des structures (qui sont comme chacun le sait de type valeur) via la syntaxe simplifiée de c# 3.0 ? En IL, on voit très clairement qu'un objet simple dérive de « [mscorlib]System.Object » alors qu'une structure dérive de « [mscorlib]System.ValueType ». Le compilateur c# pourrait-il prendre en charge une initialisation simplifiée d'une structure du point de vue de la syntaxe ?

// sans précision de l'attribut System.Runtime.InteropServices.StructLayout

       // la structure est de type séquentielle

       public struct MaStructure

       {

           public int ValeurA;

           public byte ValeurB;

           public int ValeurC;

       }

 

       static void Main(string[] args)

       {

           var a = new MaStructure {ValeurA=256,ValeurB=255,ValeurC=45};

           Console.WriteLine(a.ValeurA.ToString());

           Console.ReadLine();

       }

Cette écriture est donc possible et est bien agréable. Mais que génère-t-elle ?

.class sequential ansi sealed nested public beforefieldinit MaStructure

       extends [mscorlib]System.ValueType

{

  .field public int32 ValeurA

  .field public uint8 ValeurB

  .field public int32 ValeurC

}

 

.method private hidebysig static void  Main(string[] args) cil managed

{

  .entrypoint

  .maxstack  2

  .locals init ([0] valuetype ConsoleApplication1.Program/MaStructure a,

           [1] valuetype ConsoleApplication1.Program/MaStructure '<>g__initLocal0', //Variable locale supplémentaire

           [2] valuetype ConsoleApplication1.Program/MaStructure CS$0$0000)  // variable locale supplémentaire

  IL_0001:  ldloca.s   CS$0$0000

  IL_0003:  initobj    ConsoleApplication1.Program/MaStructure

  IL_0009:  ldloc.2 // chargement de la variable locale 2

  IL_000a:  stloc.1   // dans la variable locale 1

  IL_000b:  ldloca.s   '<>g__initLocal0'

  IL_000d:  ldc.i4    0x100

  IL_0012:  stfld      int32 ConsoleApplication1.Program/MaStructure::ValeurA

  IL_0017:  ldloca.s   '<>g__initLocal0'

  IL_0019:  ldc.i4    0xff

  IL_001e:  stfld      uint8 ConsoleApplication1.Program/MaStructure::ValeurB

  IL_0023:  ldloca.s   '<>g__initLocal0'

  IL_0025:  ldc.i4.s   45

  IL_0027:  stfld      int32 ConsoleApplication1.Program/MaStructure::ValeurC

  IL_002c:  ldloc.1   // Chargement de la variable locale 1

  IL_002d:  stloc.0   // dans la variable locale 0 "a"

  IL_002e:  ldloca.s   a

  IL_0030:  ldflda    int32 ConsoleApplication1.Program/MaStructure::ValeurA

  IL_0035:  call       instance string [mscorlib]System.Int32::ToString()

  IL_003a:  call       void [mscorlib]System.Console::WriteLine(string)

  IL_0040:  call       string [mscorlib]System.Console::ReadLine()

  IL_0045:  pop

  IL_0046:  ret

}

Le lecteur peu habitué au langage intermédiaire IL s'aidera des commentaires des précédents codes IL pour interpréter ce code. Il apparaît clairement que trois variables locales ont été définies alors qu'une seule aurait été suffisante. Ceci engendre deux inutiles chargements et déchargements dans la pile d'évaluation de la méthode, soit quatre instructions. Nous allons toutefois vérifier que l'écriture à l'ancienne génère bien un code IL bien plus allégé.

// sans précision de l'attribut System.Runtime.InteropServices.StructLayout

// la structure est de type séquentielle

public struct MaStructure

{

    public int ValeurA;

    public byte ValeurB;

    public int ValeurC;

}

 

static void Main(string[] args)

{

    var a = new MaStructure();

    a.ValeurA = 256;

    a.ValeurB = 255;

    a.ValeurC = 45;

 

    Console.WriteLine(a.ValeurA.ToString());

    Console.ReadLine();

}

dont le code IL correspondant est le suivant :

.class sequential ansi sealed nested public beforefieldinit MaStructure

       extends [mscorlib]System.ValueType

{

  .field public int32 ValeurA

  .field public uint8 ValeurB

  .field public int32 ValeurC

}

 

.method private hidebysig static void  Main(string[] args) cil managed

{

  .entrypoint

  .maxstack  2

  .locals init ([0] valuetype ConsoleApplication1.Program/MaStructure a)

  IL_0001:  ldloca.s   a

  IL_0003:  initobj    ConsoleApplication1.Program/MaStructure

  IL_0009:  ldloca.s   a

  IL_000b:  ldc.i4    0x100

  IL_0010:  stfld      int32 ConsoleApplication1.Program/MaStructure::ValeurA

  IL_0015:  ldloca.s   a

  IL_0017:  ldc.i4    0xff

  IL_001c:  stfld      uint8 ConsoleApplication1.Program/MaStructure::ValeurB

  IL_0021:  ldloca.s   a

  IL_0023:  ldc.i4.s   45

  IL_0025:  stfld      int32 ConsoleApplication1.Program/MaStructure::ValeurC

  IL_002a:  ldloca.s   a

  IL_002c:  ldflda    int32 ConsoleApplication1.Program/MaStructure::ValeurA

  IL_0031:  call       instance string [mscorlib]System.Int32::ToString()

  IL_0036:  call       void [mscorlib]System.Console::WriteLine(string)

  IL_003c:  call       string [mscorlib]System.Console::ReadLine()

  IL_0041:  pop

  IL_0042:  ret

}

Effectivement, le code à l'ancienne semble être plus performant. Ceux d'entre vous qui auront voulu vérifier via l'utilitaire Reflector seront passés complètement à côté de la démonstration précédente puisque votre utilitaire préféré génère le code c# suivant à partir de l'écriture c# 3.0 :

private static void Main(string[] args)

{

    MaStructure <>g__initLocal1 = new MaStructure();

    <>g__initLocal1.ValeurA = 0x100;

    <>g__initLocal1.ValeurB = 0xff;

    <>g__initLocal1.ValeurC = 0x2d;

    Console.WriteLine(<>g__initLocal1.ValeurA.ToString());

    Console.ReadLine();

}

qui montre clairement une seule variable locale ! Ceux qui auront le bon réflexe de regarder le code IL proposé par Reflector ne seront pas passés à côté des trois variables locales.
Les plus critiques d'entre vous pourront se dire que c'est le compilateur JIT qui doit réaliser une optimisation lors de l'exécution. On doit l'espérer en effet, car lorsqu'on travaille avec des types valeur et non des types référence, ce n'est pas une simple référence qui est copiée mais bien tous les éléments de notre structure. Dans notre cas ci-dessus, elle est donc copiée deux fois inutilement. Regardons ce que génère le JIT pour le code à l'ancienne sur mon ordinateur Dual Core 32 bits :

00000000  push        ebp  // On se situe dans un sous-programme, la valeur BP est mise sur la pile

00000001  mov        ebp,esp // On donne à BP la valeur SP

00000003  push        edi // sauvegarde des registres

00000004  push        esi 

00000005  sub        esp,10h // Déplace le pointeur de pile

00000008  xor        eax,eax // eax est mis à zéro

0000000a  mov        dword ptr [ebp-18h],eax //mise à zero de la zone correspondant à ValeurA

0000000d  mov        dword ptr [ebp-14h],eax //mise à zero de la zone correspondant à ValeurB

//(A noter qu'on remarque bien le sequentiel de la structure)

00000010  mov        dword ptr [ebp-10h],eax //mise à zéro de la zone correspondant à ValeurC

00000013  mov        dword ptr [ebp-0Ch],ecx

00000016  cmp        dword ptr ds:[00918A50h],0 // regarde si l'adresse est à zero

0000001d  je          00000024 // en cas d'égalité va en 00000024

0000001f  call        76E1F369 // Appel d'un sous-programme

00000024  lea        edi,[ebp-18h] // di pointe sur la position -18h de la pile

00000027  xor        eax,eax // eax est remis à zero

00000029  pxor        xmm0,xmm0 // le registre xmm0 du proc MMX est mis à zero

0000002d  movq        mmword ptr [edi],xmm0 //copie xmm0 dans EDI (4 mots)

00000031  add        edi,8 //on se déplace de 2 mots

00000034  stos        dword ptr es:[edi]

00000035  mov        dword ptr [ebp-18h],100h //ValeurA prendra 256

0000003c  mov        Byte ptr [ebp-14h],0FFh //ValeurB Prendra 255

00000040  mov        dword ptr [ebp-10h],2Dh //ValeurC prendra 45

00000047  lea        ecx,[ebp-18h] //ecx prend la valeur A

0000004a  call        760C15A0 //Appel du sous-programme 760C15A0 pour le ToString()

0000004f  mov        esi,eax

00000051  mov        ecx,esi

00000053  call        76138890 // appel du sous-programme pour afficher la chaîne

00000058  call        7613851C // appel à ReadLine()

0000005f  lea        esp,[ebp-8]

00000062  pop        esi  //libération de la pile

00000063  pop        edi 

00000064  pop        ebp  //restitution de ebp

00000065  ret              

Comparons ce code avec celui correspondant à la nouvelle syntaxe simplifiée de c# 3.0. :

00000000  push        ebp  // On se situe dans un sous-programme, la valeur BP est mise sur la pile

00000001  mov        ebp,esp // On donne à BP la valeur SP

00000003  push        edi  // sauvegarde des registres

00000004  push        esi 

00000005  sub        esp,28h //déplace le pointeur de pile

00000008  mov        esi,ecx // copie ecx dans esi

0000000a  lea        edi,[ebp-30h] // DI pointe sur la pile à 30h de la position initiale

0000000d  mov        ecx,9 // on donne la valeur 9 au compteur CX

00000012  xor        eax,eax  // EAX est remis à zero

00000014  rep stos    dword ptr es:[edi] // répète 9 fois le double mot à zero sur EDI

00000016  mov        ecx,esi

00000018  mov        dword ptr [ebp-0Ch],ecx // ecx est mis sur la pile

0000001b  cmp        dword ptr ds:[00918A50h],0 //regarde si c'est à zero

00000022  je          00000029 // si oui, va en 00000029

00000024  call        76E1F369 //Appel d'un sous-programme

00000029  lea        edi,[ebp-30h] //di pointe à nouveau sur la position -30h de la pile

0000002c  xor        eax,eax // mise à zero de eax

0000002e  pxor        xmm0,xmm0 // mise à zero du registre MMX

00000032  movq        mmword ptr [edi],xmm0 //copie de 4 mots à zéro

00000036  add        edi,8

00000039  stos        dword ptr es:[edi] // copie de 2 mots à zéro

0000003a  lea        edi,[ebp-24h] //di pointe sur la position -24h

0000003d  lea        esi,[ebp-30h] //SI pointe sur la position -30h à partir duquel on va copier

00000040  movq        xmm0,mmword ptr [esi]

00000044  movq        mmword ptr [edi],xmm0 // copie de 4 Mots

00000048  add        esi,8

0000004b  add        edi,8  

0000004e  movs        dword ptr es:[edi],dword ptr [esi]  //copie de 2 mots

0000004f  mov        dword ptr [ebp-24h],100h //afecte à 256

00000056  mov        Byte ptr [ebp-20h],0FFh //affecte à 255

0000005a  mov        dword ptr [ebp-1Ch],2Dh //affecte à 45

00000061  lea        edi,[ebp-18h] //di pointe sur la position -18h

00000064  lea        esi,[ebp-24h] //SI pointe sur la position -24h à partir duquel on va copier

00000067  movq        xmm0,mmword ptr [esi]

0000006b  movq        mmword ptr [edi],xmm0 // copie de 4 mots (contient les valeurs 256 et 255)

0000006f  add        esi,8

00000072  add        edi,8

00000075  movs        dword ptr es:[edi],dword ptr [esi] // copie de 2 mots (a pour valeur 45)

00000076  lea        ecx,[ebp-18h] //on place ValeurA dans ecx

00000079  call        760C15A0 //sous-programme permettant d'obtenir une chaine de caractère

0000007e  mov        esi,eax

00000080  mov        ecx,esi

00000082  call        76138890 //affichage de la chaine dans la console

00000087  call        7613851C //Appel au sous-programme pour ReadLine()

0000008e  lea        esp,[ebp-8]

00000091  pop        esi 

00000092  pop        edi 

00000093  pop        ebp  // on renvoie la valeur BP avant le retour du sous-programme

00000094  ret             

J'ai mis en évidence les instructions « supplémentaires » en rouge afin que chacun puisse se rendre compte que le compilateur JIT ne réalise aucune optimisation. Si on peut tolérer dans le cas d'une initialisation de propriétés d'un objet, la copie supplémentaire (inutile) d'une référence, l'initialisation simplifiée d'une structure pose de réels problèmes de performance.
Une question que vous pouvez vous poser est la suivante : Si j'ai dix éléments dans ma structure, aurais-je dix variables locales internes, puisque dans l'exemple précédent, nous avions trois éléments pour 3 variables ? Je vous rassure, vous aurez toujours trois variables locales internes donc une pile comportant trois fois la taille de votre structure !
Quels sont les enseignements ? Le lecteur critique n'ayant utilisé que Reflector, par bonne habitude, sera passé à côté des différences IL vu précédemment mais aussi à côté de ce qui est généré par le JIT pour le code c# 3.0 Je déconseillerai très fortement d'utiliser la syntaxe simplifiée de c# 3.0 pour initialiser une structure, car celle-ci s'accompagne d'une perte de performance notable.
 
» Démarrer une discussion
 
Discussion démarée par Malkuth le 24/07/2007 à 03:11, 2 commentaire(s).
Discussion démarée par vjacquet le 25/06/2007 à 17:42, 4 commentaire(s).