Curiosité: Pourquoi Expression est-il plus rapide qu’une DynamicMethod minimale?

Je suis en train de faire des optimisations de dernière minute, principalement pour le plaisir et l’apprentissage, et j’ai découvert quelque chose qui m’a laissé quelques questions.

Tout d’abord, les questions:

  1. Lorsque je construis une méthode en mémoire à l’aide de DynamicMethod et que j’utilise le débogueur, y a-t-il un moyen pour moi d’entrer dans le code généré lors de l’affichage du code dans la vue du désassembleur? Le débogueur semble juste surpasser toute la méthode pour moi
  2. Ou, si ce n’est pas possible, est-il possible pour moi d’enregistrer le code IL généré sur le disque en tant qu’assemblage, afin de pouvoir l’inspecter avec Reflector ?
  3. Pourquoi la version Expression de ma méthode d’ajout simple (Int32 + Int32 => Int32) est-elle plus rapide qu’une version DynamicMethod minimale?

Voici un programme court et complet qui démontre. Sur mon système, la sortie est la suivante:

 DynamicMethod: 887 ms Lambda: 1878 ms Method: 1969 ms Expression: 681 ms 

Je m’attendais à ce que les appels lambda et les appels de méthode aient des valeurs plus élevées, mais la version de DynamicMethod est constamment inférieure d’environ 30 à 50% (variations probablement dues à Windows et à d’autres programmes). Quelqu’un sait la raison?

Voici le programme:

 using System; using System.Linq.Expressions; using System.Reflection.Emit; using System.Diagnostics; namespace Sandbox { public class Program { public static void Main(Ssortingng[] args) { DynamicMethod method = new DynamicMethod("TestMethod", typeof(Int32), new Type[] { typeof(Int32), typeof(Int32) }); var il = method.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Add); il.Emit(OpCodes.Ret); Func f1 = (Func)method.CreateDelegate( typeof(Func)); Func f2 = (Int32 a, Int32 b) => a + b; Func f3 = Sum; Expression<Func> f4x = (a, b) => a + b; Func f4 = f4x.Comstack(); for (Int32 pass = 1; pass <= 2; pass++) { // Pass 1 just runs all the code without writing out anything // to avoid JIT overhead influencing the results Time(f1, "DynamicMethod", pass); Time(f2, "Lambda", pass); Time(f3, "Method", pass); Time(f4, "Expression", pass); } } private static void Time(Func fn, Ssortingng name, Int32 pass) { Stopwatch sw = new Stopwatch(); sw.Start(); for (Int32 index = 0; index <= 100000000; index++) { Int32 result = fn(index, 1); } sw.Stop(); if (pass == 2) Debug.WriteLine(name + ": " + sw.ElapsedMilliseconds + " ms"); } private static Int32 Sum(Int32 a, Int32 b) { return a + b; } } } 

    La méthode créée via DynamicMethod passe par deux thunks, tandis que la méthode créée via Expression<> ne traverse aucune.

    Voici comment cela fonctionne. Voici la séquence d’appel pour appeler fn(0, 1) dans la méthode Time (j’ai codé les arguments sur 0 et 1 pour faciliter le débogage):

     00cc032c 6a01 push 1 // 1 argument 00cc032e 8bcf mov ecx,edi 00cc0330 33d2 xor edx,edx // 0 argument 00cc0332 8b410c mov eax,dword ptr [ecx+0Ch] 00cc0335 8b4904 mov ecx,dword ptr [ecx+4] 00cc0338 ffd0 call eax // 1 arg on stack, two in edx, ecx 

    Pour la première invocation que j’ai étudiée, DynamicMethod , la ligne call eax se présente comme DynamicMethod :

     00cc0338 ffd0 call eax {003c2084} 0:000> !u 003c2084 Unmanaged code 003c2084 51 push ecx 003c2085 8bca mov ecx,edx 003c2087 8b542408 mov edx,dword ptr [esp+8] 003c208b 8b442404 mov eax,dword ptr [esp+4] 003c208f 89442408 mov dword ptr [esp+8],eax 003c2093 58 pop eax 003c2094 83c404 add esp,4 003c2097 83c010 add eax,10h 003c209a ff20 jmp dword ptr [eax] 

    Cela semble faire un tour de stack pour réorganiser les arguments. Je suppose que c’est dû à la différence entre les delegates qui utilisent l’argument implicite «this» et ceux qui ne le font pas.

    Ce saut à la fin résout comme ça:

     003c209a ff20 jmp dword ptr [eax] ds:0023:012f7edc=0098c098 0098c098 e963403500 jmp 00ce0100 

    Le rest du code à 0098c098 ressemble à un thunk JIT, dont le début a été réécrit avec un jmp après le JIT. Ce n’est qu’après ce saut que nous arrivons au code réel:

     0:000> !u eip Normal JIT generated code DynamicClass.TestMethod(Int32, Int32) Begin 00ce0100, size 5 >>> 00ce0100 03ca add ecx,edx 00ce0102 8bc1 mov eax,ecx 00ce0104 c3 ret 

    La séquence d’appel de la méthode créée via Expression<> est différente: il manque le code de swizzling de la stack. Voilà, dès le premier saut via eax :

     00cc0338 ffd0 call eax {00ce00a8} 0:000> !u eip Normal JIT generated code DynamicClass.lambda_method(System.Runtime.ComstackrServices.ExecutionScope, Int32, Int32) Begin 00ce00a8, size b >>> 00ce00a8 8b442404 mov eax,dword ptr [esp+4] 00ce00ac 03d0 add edx,eax 00ce00ae 8bc2 mov eax,edx 00ce00b0 c20400 ret 4 

    Maintenant, comment ça s’est passé comme ça?

    1. Le swizzling de stack n’était pas nécessaire (le premier argument implicite du délégué est réellement utilisé, c.-à-d. Pas comme un délégué lié à une méthode statique)
    2. Le JIT doit avoir été forcé par la logique de compilation LINQ pour que le délégué détienne l’adresse de destination réelle plutôt qu’une fausse.

    Je ne sais pas comment le LINQ a forcé le JIT, mais je sais comment forcer un JIT en appelant la fonction au moins une fois. MISE À JOUR: J’ai trouvé un autre moyen de forcer un JIT: utiliser l’argumetn ressortingctedSkipVisibility pour le constructeur et passer true . Donc, voici le code modifié qui élimine le swizzling de la stack en utilisant le paramètre implicite “this”, et utilise le constructeur alternatif pour la pré-compilation afin que l’adresse liée soit l’adresse réelle, plutôt que le thunk:

     using System; using System.Linq.Expressions; using System.Reflection.Emit; using System.Diagnostics; namespace Sandbox { public class Program { public static void Main(Ssortingng[] args) { DynamicMethod method = new DynamicMethod("TestMethod", typeof(Int32), new Type[] { typeof(object), typeof(Int32), typeof(Int32) }, true); var il = method.GetILGenerator(); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Add); il.Emit(OpCodes.Ret); Func f1 = (Func)method.CreateDelegate( typeof(Func), null); Func f2 = (Int32 a, Int32 b) => a + b; Func f3 = Sum; Expression> f4x = (a, b) => a + b; Func f4 = f4x.Comstack(); for (Int32 pass = 1; pass < = 2; pass++) { // Pass 1 just runs all the code without writing out anything // to avoid JIT overhead influencing the results Time(f1, "DynamicMethod", pass); Time(f2, "Lambda", pass); Time(f3, "Method", pass); Time(f4, "Expression", pass); } } private static void Time(Func fn, Ssortingng name, Int32 pass) { Stopwatch sw = new Stopwatch(); sw.Start(); for (Int32 index = 0; index < = 100000000; index++) { Int32 result = fn(index, 1); } sw.Stop(); if (pass == 2) Console.WriteLine(name + ": " + sw.ElapsedMilliseconds + " ms"); } private static Int32 Sum(Int32 a, Int32 b) { return a + b; } } } 

    Voici les temps d'exécution sur mon système:

     DynamicMethod: 312 ms Lambda: 417 ms Method: 417 ms Expression: 312 ms 

    Mis à jour pour append :

    J'ai essayé d'exécuter ce code sur mon nouveau système, qui est un Core i7 920 exécutant Windows 7 x64 avec .NET 4 beta 2 installé (mscoree.dll version 4.0.30902), et les résultats sont bien variables.

     csc 3.5, /platform:x86, runtime v2.0.50727 (via .config) Run #1 DynamicMethod: 214 ms Lambda: 571 ms Method: 570 ms Expression: 249 ms Run #2 DynamicMethod: 463 ms Lambda: 392 ms Method: 392 ms Expression: 463 ms Run #3 DynamicMethod: 463 ms Lambda: 570 ms Method: 570 ms Expression: 463 ms 

    C'est peut-être ce qui affecte les résultats d'Intel SpeedStep, ou peut-être Turbo Boost. En tout cas, c'est très ennuyant.

     csc 3.5, /platform:x64, runtime v2.0.50727 (via .config) DynamicMethod: 428 ms Lambda: 392 ms Method: 392 ms Expression: 428 ms csc 3.5, /platform:x64, runtime v4 DynamicMethod: 428 ms Lambda: 356 ms Method: 356 ms Expression: 428 ms csc 4, /platform:x64, runtime v4 DynamicMethod: 428 ms Lambda: 356 ms Method: 356 ms Expression: 428 ms csc 4, /platform:x86, runtime v4 DynamicMethod: 463 ms Lambda: 570 ms Method: 570 ms Expression: 463 ms csc 3.5, /platform:x86, runtime v4 DynamicMethod: 214 ms Lambda: 570 ms Method: 571 ms Expression: 249 ms 

    Bon nombre de ces résultats seront des accidents de synchronisation, peu importe ce qui provoque les accélérations aléatoires dans le scénario C # 3.5 / runtime v2.0. Je devrai redémarrer pour voir si SpeedStep ou Turbo Boost est responsable de ces effets.