2009-08-18 12 views
52

Ich mache derzeit einige Optimierungsmaßnahmen, hauptsächlich zum Spaß und zum Lernen, und habe etwas entdeckt, das mich mit ein paar Fragen zurückgelassen hat.Kuriosität: Warum wird Expression <...> bei der Kompilierung schneller als eine minimale DynamicMethod ausgeführt?

Zunächst werden die Fragen:

  1. Wenn ich eine Methode in-Speicher durch die Verwendung von DynamicMethod konstruieren, und der Debugger verwenden, gibt es eine Möglichkeit für mich in den erzeugten Assembler-Code zu Schritt, wenn vieweing der Code in der Disassembler-Ansicht? Der Debugger scheint nur über die ganze Methode für mich zu gehen
  2. Oder, wenn das nicht möglich ist, ist es mir möglich, den generierten IL-Code auf der Festplatte als eine Baugruppe zu speichern, so dass ich es mit Reflector überprüfen kann?
  3. Warum läuft die Expression<...>-Version meiner einfachen Additionsmethode (Int32 + Int32 => Int32) schneller als eine minimale DynamicMethod-Version?

Hier ist ein kurzes und komplettes Programm, das demonstriert. Auf meinem System ist die Ausgabe:

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

ich das Lambda und Verfahren zu erwarten fordern höhere Werte zu haben, aber die Dynamic Version ist durchweg etwa 30-50% langsamer (Variationen wahrscheinlich aufgrund von Windows und anderen Programmen). Wer kennt den Grund?

Hier ist das Programm:

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; 
     } 
    } 
} 
+1

Interessante Frage. Diese Dinge können mit WinDebug und SOS gelöst werden. Ich veröffentlichte eine Schritt für Schritt eine ähnliche Analyse, die ich vor vielen Monden in meinem Blog gemacht habe, http://blog.barrkel.com/2006/05/clr-tailcall-optimization-or-lack.html –

+0

Ich dachte, ich sollte pingen Du - Ich habe herausgefunden, wie man JIT erzwingt, ohne die Methode einmal aufrufen zu müssen. Verwenden Sie das Konstruktorargument 'restrictedSkipVisibility' DynamicMethod. Je nach Kontext (Codesicherheit) ist es jedoch möglicherweise nicht verfügbar. –

+1

Wirklich gute Frage. Zuerst würde ich für diese Art von Profiling ein Release/Console verwenden - also sieht die 'Debug.WriteLine' fehl; aber selbst mit 'Console.WriteLine' sind meine Werte ähnlich: DynamicMethod: 630 ms Lambda: 561 ms Methode: 553 ms Ausdruck: 360 ms Ich suche noch ... –

Antwort

53

Das Verfahren über DynamicMethod erstellt zwei Thunks durchläuft, während das Verfahren über Expression<> erstellt nicht durch irgendwelche geht.

So funktioniert es. Hier ist die Rufsequenz fn(0, 1) im Time Verfahren zum Aufrufen (I hart codiert, die Argumente zu 0 und 1 für eine einfache Fehlersuche):

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 

Für den ersten Aufruf I untersucht, DynamicMethod, die call eax Linie kommt wie Also:

Dies scheint zu tun, einige Stapel swizzling, um Argumente neu anzuordnen. Ich spekuliere, dass dies auf den Unterschied zwischen den Delegierten zurückzuführen ist, die das implizite Argument "this" verwenden, und denen, die dies nicht tun.

, die am Ende springen löst etwa so:

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

Der Rest des Codes bei 0098c098 wie ein JIT thunk aussieht, Anfang das wurde neu geschrieben mit einem jmp nach dem JIT. Es ist erst nach diesem Sprung, den wir echten Code erhalten:

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 

Der Aufruf-Sequenz für die über Expression<> erstellte Methode unterschiedlich ist - es den Stapel Swizzling Code fehlt. Hier ist es, aus dem ersten Sprung über 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 

Nun, wie haben Dinge wie diese bekommen?

  1. Stapel Swizzling war nicht notwendig (das implizite erste Argument vom Delegierten tatsächlich verwendet wird, also nicht einen Delegierten zu einer statischen Methode bestimmt sein wie)
  2. Der JIT muss so durch LINQ compilation Logik gezwungen worden, dass der Delegierte hatte die echte Zieladresse statt eine falsche Adresse.

Ich weiß nicht, wie der LINQ den JIT erzwang, aber ich weiß, wie man einen JIT selbst erzwingt - indem ich die Funktion mindestens einmal anrufe. UPDATE: Ich habe einen anderen Weg gefunden, einen JIT zu erzwingen: benutze den restrictedSkipVisibility argumetn zum Konstruktor und passiere true. So, hier ist modifizierte Code, die mit dem impliziten ‚dieses‘ Parameterstapel Swizzling eliminiert, und verwendet den alternativen Konstruktor vorab zusammenstellen, so dass die gebundene Adresse die reale Adresse ist, anstatt die 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; 
     } 
    } 
} 

Hier die Laufzeiten auf meinem System:

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

AKTUALISIERT hinzu:

ich diesen Code auf meinem neuen System versucht mit, die ein Core i7 920 mit Windows 7 x64 mit .NET 4 beta 2 installiert (m scorte.dll ver. 4.0.30902), und die Ergebnisse sind, naja, variabel.

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 

Vielleicht ist dies Intel SpeedStep Auswirkungen auf die Ergebnisse oder möglicherweise Turbo Boost. Auf jeden Fall ist es sehr nervig.

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 

Viele dieser Ergebnisse werden Unfälle von Timing sein, was auch immer es ist, dass der Zufall speedups in dem C# 3.5/Runtime v2.0 Szenario verursacht. Ich muss neu starten, um zu sehen, ob SpeedStep oder Turbo Boost für diese Effekte verantwortlich ist.

+0

Das bedeutet, dass ich eine Methode hinzufügen muss, um meine Methode sicher aufzurufen, nur um diesen Leistungsschub zu erhalten? Das kann ich sicher tun. –

+1

Was ich meine ist ... die Methoden, die ich erstelle, werden eigentlich nicht zwei Zahlen summieren, sondern für die Erstellung und Lösung von Diensten in einer IoC-Implementierung verantwortlich sein. In diesem Fall möchte ich nicht, dass die vollständige Methode ausgeführt und ein Dienst erstellt wird, nur um diesen kleinen Leistungsschub zu erhalten. Da einige Dienste verwendet werden * eine Menge *, und der eigentliche Dienst ist winzig und leicht, ich gebe einige Anstrengungen in die tatsächliche Auflösung Code. Außerdem ist es ein lustiges Lernprojekt für reflection.emit. Schätze die Arbeit, die du in deine Antwort gesteckt hast! –

+4

Eine faszinierende und gründliche Analyse. Danke –

Verwandte Themen