2009-08-18 38 views
52

我目前正在做一些最後的措施優化,主要是爲了好玩和學習,並發現了一些讓我留下幾個問題的東西。好奇心:爲什麼表達式<...>編譯時運行速度比最小的DynamicMethod快?

首先,問題:

  1. 當我構造的內存中的方法通過使用DynamicMethod,並使用調試器,是否有任何方式爲我vieweing當踏入生成的彙編代碼,反彙編視圖中的代碼?調試器似乎只是對我整個方法的步驟
  2. 或者,如果這是不可能的,是否有可能以某種方式將生成的IL代碼作爲程序集保存到磁盤,以便我可以用Reflector檢查它?
  3. 爲什麼我的簡單加法方法(Int32 + Int32 => Int32)的Expression<...>版本比最小的DynamicMethod版本運行得更快?

下面是一個簡短而完整的程序演示。在我的系統,輸出爲:

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

我預期的λ和方法調用具有更高的價值,但DynamicMethod的版本是一致慢約30-50%(的變化可能是由於Windows和其他程序)。任何人都知道原因?

這裏的程序:

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

有趣的問題。使用WinDebug和SOS可以解決這些問題。我發佈了一個類似分析的步驟,我在我的博客中進行了許多個月前的測試,http://blog.barrkel.com/2006/05/clr-tailcall-optimization-or-lack.html – 2009-08-18 23:25:22

+0

我想我應該ping你 - 我發現如何強制JIT而不必調用該方法一次。使用'restrictedSkipVisibility' DynamicMethod構造函數參數。根據上下文(代碼安全性),它可能不可用。 – 2009-08-19 13:43:13

+1

真的很好的問題。首先,對於這種類型的分析,我會使用release/Console - 所以'Debug.WriteLine'看起來不合適;但即使'Console.WriteLine'我的統計是類似的:DynamicMethod:630毫秒Lambda:561毫秒方法:553毫秒錶達式:360毫秒我仍在尋找... – 2009-08-18 21:49:15

回答

53

通過DynamicMethod創建的方法將通過2層的thunk,同時通過Expression<>創建的方法不經過任何。

這是它的工作原理。下面是在Time方法調用fn(0, 1)調用序列(我硬編碼的參數爲0和1爲便於調試):

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 

對於第一次調用我的調查,DynamicMethodcall eax線出現像所以:

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] 

這似乎是做一些堆棧調整參數重新排列。我推測這是由於使用隱含的'this'參數的代表和不代表的代表之間的區別。

這末跳解決像這樣:

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

在0098c098將其餘的代碼看起來像一個JIT的thunk,其開始在JIT後,用jmp得到改寫。這只是這個跳躍,我們得到真正的代碼之後:

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 

經由Expression<>創建的方法調用順序是不同的 - 缺少堆棧混寫代碼。這是從第一跳通過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 

現在,怎麼事情就這樣嗎?

  1. 堆棧混寫是沒有必要的(從委託隱含第一個參數被實際使用,即不象結合到一個靜態方法的委託)
  2. 的JIT必須被迫通過LINQ編譯邏輯,這樣代表持有真正的目的地地址,而不是假的。

我不知道該怎麼LINQ迫使JIT,但我不知道如何強制JIT自己 - 通過調用函數至少一次。更新:我發現另一種強制JIT的方法:使用restrictedSkipVisibility argumetn構造函數並通過true。所以,這裏是通過使用隱式「這個」參數消除堆混寫修改後的代碼,並使用可選的構造預編譯,這樣綁定的地址是真實的地址,而不是在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; 
     } 
    } 
} 

這裏的

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

更新,增加了:我的系統上運行時間

我試圖運行我的新系統,它運行的是Windows 7 64位系統安裝.NET 4 beta 2版本的酷睿i7 920在此代碼(M scoree.dll ver。 4.0.30902),結果是可變的。

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 

也許這是Intel SpeedStep影響結果,或者可能是Turbo Boost。無論如何,這是非常煩人的。

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 

這些結果中的很多都會導致計時事故,無論是導致C#3.5/runtime v2.0場景中的隨機加速。我將不得不重新啓動以查看SpeedStep或Turbo Boost是否對這些影響負責。

+0

這意味着我需要添加一種方法來安全地調用我的方法,只是爲了獲得性能提升?我當然可以做到這一點。 – 2009-08-19 08:24:27

+1

我的意思是......我創建的方法實際上並不是總結兩個數字,而是負責構建和解析IoC實現中的服務。在這種情況下,我並不是真的想要完整的方法來執行和構建服務,只是爲了獲得微小的性能提升。看到一些服務將被使用*很多*,而實際的服務是微乎其微的,我正在爲實際的解析代碼做些努力。此外,這是一個有趣的學習項目reflection.emit。真的很感謝你在答案中所做的工作! – 2009-08-19 08:27:32

+4

迷人而深入的分析。謝謝 – 2009-08-19 08:48:39

相關問題