53 votes

Curiosité: Pourquoi Expression <...> lors de la compilation est-elle plus rapide qu'un DynamicMethod minimal?

Je suis actuellement en train de faire quelques derniers-mesurer les optimisations, principalement pour le plaisir et l'apprentissage, et découvert quelque chose qui m'a laissé avec un couple de questions.

Tout d'abord, les questions:

  1. Quand je construis une méthode en mémoire grâce à l'utilisation de DynamicMethod, et utiliser le débogueur, est-il possible pour moi d'entrer dans le code assembleur généré, quand vieweing le code dans le désassembleur vue? Le débogueur semble juste d'étape sur l'ensemble de la méthode pour moi
  2. Ou, si cela n'est pas possible, est-il possible pour moi de faire en quelque sorte enregistrer le code généré IL sur le disque sous forme d'un assemblage, de sorte que je puisse l'inspecter avec Réflecteur?
  3. Pourquoi ne l' Expression<...> version de mon simple ajout de la méthode (Int32+Int32 => Int32), courir plus vite qu'un minimum DynamicMethod version?

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

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

Je m'attendais à la lambda et les appels de méthode pour avoir des valeurs plus élevées, mais la DynamicMethod version est de façon constante d'environ 30 à 50% plus lent (variations probablement en raison de Windows et d'autres programmes). Quelqu'un connait 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(String[] 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<Int32, Int32, Int32> f1 =
                (Func<Int32, Int32, Int32>)method.CreateDelegate(
                    typeof(Func<Int32, Int32, Int32>));
            Func<Int32, Int32, Int32> f2 = (Int32 a, Int32 b) => a + b;
            Func<Int32, Int32, Int32> f3 = Sum;
            Expression<Func<Int32, Int32, Int32>> f4x = (a, b) => a + b;
            Func<Int32, Int32, Int32> f4 = f4x.Compile();
            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<Int32, Int32, Int32> fn,
            String 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;
        }
    }
}

54voto

Barry Kelly Points 30330

La méthode créée par DynamicMethod passe par deux thunks, tandis que la méthode créée par Expression<> n'est pas tout.

Voici comment cela fonctionne. Voici la séquence d'appel pour appeler l' fn(0, 1) dans la Time méthode (j'ai codé en dur les arguments de 0 et de 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, j'ai étudié, DynamicMethod, call eax ligne:

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 être en train de faire certains pile swizzling pour réorganiser les arguments. Je suppose que c'est en raison de la différence entre les délégués qui utilisent l'implicite de cet argument et ceux qui n'en ont pas.

Que de sauter à la fin résout comme suit:

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

Le reste du code à 0098c098 ressemble à un JIT thunk, dont le départ l'a réécrit avec un jmp après le JIT. C'est seulement après ce saut que nous arrivons à vrai code:

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

L'invocation de la séquence de la méthode créée par Expression<> est différent: il manque la pile swizzling code. Ici, il est, à partir du premier saut via l' eax:

00cc0338 ffd0            call    eax {00ce00a8}

0:000> !u eip
Normal JIT generated code
DynamicClass.lambda_method(System.Runtime.CompilerServices.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 les choses deviennent de ce genre?

  1. Pile swizzling n'était pas nécessaire (l'implicite premier argument du délégué est effectivement utilisé, c'est à dire pas comme un délégué à une méthode statique)
  2. L'équipe doit avoir été forcé par LINQ compilation logique de sorte que le délégué a tenu la véritable adresse de destination plutôt que d'un faux.

Je ne sais pas comment le LINQ forcé l'équipe, mais je ne sais comment faire pour forcer un JIT moi - même- par appel de la fonction, au moins une fois. Mise à JOUR: j'ai trouvé un autre moyen de forcer un JIT: utilisation de l' restrictedSkipVisibility argumetn par le constructeur et pass true. Voici donc modifié le code qui élimine pile swizzling en utilisant l'implicite de ce paramètre, et utilise l'autre constructeur de pré-compilation, de sorte que la limite de l'adresse est l'adresse réelle, plutôt que de le thunk:

using System;
using System.Linq.Expressions;
using System.Reflection.Emit;
using System.Diagnostics;

namespace Sandbox
{
    public class Program
    {
        public static void Main(String[] 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<Int32, Int32, Int32> f1 =
                (Func<Int32, Int32, Int32>)method.CreateDelegate(
                    typeof(Func<Int32, Int32, Int32>), null);
            Func<Int32, Int32, Int32> f2 = (Int32 a, Int32 b) => a + b;
            Func<Int32, Int32, Int32> f3 = Sum;
            Expression<Func<Int32, Int32, Int32>> f4x = (a, b) => a + b;
            Func<Int32, Int32, Int32> f4 = f4x.Compile();
            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<Int32, Int32, Int32> fn,
            String 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 le temps de fonctionnement sur mon système:

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

MIS À JOUR POUR AJOUTER:

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

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 Intel SpeedStep affectant les résultats, ou, éventuellement, de la technologie Turbo Boost. En tout cas, c'est très ennuyeux.

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

Beaucoup de ces résultats sera accidents du moment, quel qu'il soit, qui est à l'origine de l'aléatoire de la vitesse dans le C# 3.5 / runtime v2.0 scénario. Je vais avoir à redémarrer pour voir si SpeedStep ou Turbo Boost est responsable de ces effets.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X