2009-08-18 15 views
52

Obecnie przeprowadzam optymalizacje ostatniej miary, głównie dla zabawy i nauki, i odkryłem coś, co pozostawiło mnie z kilkoma pytaniami.Ciekawość: Dlaczego funkcja Expression <...> podczas kompilacji działa szybciej niż minimalna metoda DynamicMethod?

pierwsze, pytania:

  1. Kiedy skonstruować metodę w pamięci dzięki zastosowaniu DynamicMethod i używać debuggera, czy jest jakiś sposób dla mnie krok do wygenerowanego kodu montażu, kiedy vieweing kod w widoku deasemblera? Debugger wydaje się po prostu przechodzić przez całą metodę dla mnie
  2. Albo, jeśli to nie jest możliwe, czy mogę w jakiś sposób zapisać wygenerowany kod IL na dysku jako zespół, tak, że mogę sprawdzić go za pomocą Reflector?
  3. Dlaczego moja prosta metoda dodawania (Int32 + Int32 => Int32) w wersji Expression<...> działa szybciej niż minimalna wersja DynamicMethod?

Oto krótki i kompletny program, który pokazuje. W moim systemie, wyjście jest:

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

Spodziewałem lambda i metoda nazywa mieć wyższe wartości, ale wersja DynamicMethod jest stale około 30-50% wolniej (prawdopodobnie ze względu na zmiany w systemie Windows i innych programów). Ktoś zna przyczynę?

Oto program:

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

Interesujące pytanie. Tego typu rzeczy można rozwiązać za pomocą WinDebug i SOS. Przedstawiłem krok po kroku podobną analizę, którą zrobiłem wiele miesięcy temu na moim blogu, http://blog.barrkel.com/2006/05/clr-tailcall-optimization-lub-lack.html –

+0

Myślałem, że powinienem pingować ty - dowiedziałem się, jak wymusić JIT bez konieczności wywoływania metody raz. Użyj argumentu konstruktora 'restrictedSkipVisibility' DynamicMethod. W zależności od kontekstu (zabezpieczenia kodu), może nie być dostępna. –

+1

Naprawdę dobre pytanie. Po pierwsze, dla tego typu profilowania użyłbym release/Console - więc 'Debug.WriteLine' wygląda nie na miejscu; ale nawet z 'Console.WriteLine' moje statystyki są podobne: DynamicMethod: 630 ms Lambda: 561 ms Metoda: 553 ms Wyrażenie: 360 ms Ciągle szukam ... –

Odpowiedz

53

Metoda stworzona przez DynamicMethod przechodzi dwoma łącznikami, natomiast metoda stworzona przez Expression<> nie przechodzą żadnych.

Oto jak to działa. Oto sekwencja wywoławcza powołując fn(0, 1) w metodzie Time (I zakodowane argumenty do 0 i 1 dla ułatwienia debugowania):

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 

Na pierwszym wezwaniem ja badane, DynamicMethod linia call eax podchodzi jak więc:

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] 

Wygląda na to, że robię trochę swizzlingu stosu, aby zmienić kolejność argumentów. Spekuluję, że wynika to z różnicy między delegatami, którzy używają ukrytego "tego" argumentu, a tymi, którzy tego nie robią.

To przeskoczyć na koniec rozwiązuje tak:

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

Pozostała część kodu na 0098c098 wygląda thunk JIT, którego początek został przepisany z jmp po JIT. Dopiero po tym skoku, że mamy do rzeczywistego kodu:

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 

Sekwencja wywołania dla metody stworzonej przez Expression<> jest inna - brakuje kodu stos swizzling. Oto ona, od pierwszego skoku poprzez 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 

Teraz, jak nie robi się w ten sposób?

  1. swizzling Stos nie było konieczne (niejawny pierwszy argument z delegata jest faktycznie używany, czyli nie tak jak delegata związany z metodą statyczną)
  2. JIT zostały zmuszone przez LINQ kompilacji logiki, tak aby delegat posiadał prawdziwy adres docelowy, a nie fałszywy.

Nie wiem, jak LINQ zmusiło JIT, ale ja wiem, jak wymusić JIT samemu - przez wywołanie funkcji co najmniej raz. AKTUALIZACJA: Znalazłem inny sposób wymuszenia JIT: użyj argumetn restrictedSkipVisibility do konstruktora i przeprowadź true. Tak więc, oto zmodyfikowany kod, który eliminuje stosu swizzling za pomocą niejawny „this” parametr, a korzysta z alternatywnego konstruktora wstępnie skompilować tak, że granica adres jest prawdziwy adres, zamiast 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; 
     } 
    } 
} 

tutaj te czasy pracy na moim systemie:

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

zaktualizowana DODAJ:

Próbowałem ten kod działa na moim nowym systemie, który jest Core i7 920 z systemem Windows 7 x64 z .NET 4 beta 2 zainstalowany (m scoree.dll ver. 4.0.30902), a wyniki są, no cóż, zmienne.

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 

Być może to jest Intel SpeedStep wpływające na wyniki lub ewentualnie Turbo Boost. W każdym razie jest to bardzo denerwujące.

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 

Wiele z tych wyników będzie wypadki czasu, cokolwiek to jest, że powoduje losowe speedups w scenariuszu v2.0 C# 3.5/wykonawczego. Będę musiał zrestartować komputer, aby sprawdzić, czy SpeedStep lub Turbo Boost są odpowiedzialne za te efekty.

+0

Oznacza to, że muszę dodać sposób, aby bezpiecznie wywołać moją metodę, tylko po to, aby zwiększyć wydajność? Z pewnością mogę to zrobić. –

+1

Chodzi o to, że ... metody, które tworzę, nie będą sumować dwóch liczb, ale będą odpowiedzialne za konstruowanie i rozwiązywanie usług w implementacji IoC. W tym przypadku tak naprawdę nie chcę pełnej metody wykonania i skonstruowania usługi, aby uzyskać niewielki wzrost wydajności. Widząc, że niektóre usługi będą używane * dużo *, a rzeczywista usługa jest mała i lekka, wkładam również pewien wysiłek w rzeczywisty kod rozdzielczości. Poza tym jest to zabawny projekt edukacyjny dla reflection.emit. Naprawdę doceniam pracę, którą wkładasz w swoją odpowiedź! –

+4

Fascynująca i dogłębna analiza. Dzięki –

Powiązane problemy