2009-08-18 21 views
52

Tôi hiện đang thực hiện một số biện pháp tối ưu hóa cuối cùng, chủ yếu là để giải trí và học tập, và khám phá điều gì đó khiến tôi có một vài câu hỏi.Sự tò mò: Tại sao biểu thức <...> khi được biên dịch chạy nhanh hơn một DynamicMethod tối thiểu?

Thứ nhất, các câu hỏi:

  1. Khi tôi xây dựng một phương pháp trong bộ nhớ thông qua việc sử dụng các DynamicMethod, và sử dụng trình gỡ lỗi, có cách nào cho tôi để bước vào mã lắp ráp tạo ra, khi vieweing mã trong khung nhìn disassembler? Trình gỡ rối dường như chỉ cần bước qua toàn bộ phương thức cho tôi
  2. Hoặc nếu điều đó là không thể, tôi có thể tiết kiệm mã IL đã tạo thành đĩa như một assembly, để tôi có thể kiểm tra nó với Reflector?
  3. Tại sao phiên bản Expression<...> của phương pháp bổ sung đơn giản của tôi (Int32 + Int32 => Int32) chạy nhanh hơn phiên bản DynamicMethod tối thiểu?

Đây là chương trình ngắn và đầy đủ minh họa. Trên hệ thống của tôi, đầu ra là:

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

tôi mong đợi các lambda và phương pháp gọi là có giá trị cao hơn, nhưng phiên bản DynamicMethod chậm liên tục khoảng 30-50% (biến thể có thể là do Windows và các chương trình khác). Có ai biết lý do không?

Dưới đây là các chương trình:

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

Câu hỏi thú vị. Những thứ này có thể được giải quyết bằng WinDebug và SOS. Tôi đăng một bước của một phân tích tương tự tôi đã làm nhiều vệ tinh trước đây trong blog của tôi, http://blog.barrkel.com/2006/05/clr-tailcall-optimization-or-lack.html –

+0

Tôi nghĩ rằng tôi nên ping bạn - tôi phát hiện ra cách ép buộc JIT mà không phải gọi phương thức một lần. Sử dụng đối số constructor 'restrictedSkipVisibility' DynamicMethod. Tùy thuộc vào ngữ cảnh (bảo mật mã), nó có thể không có sẵn mặc dù. –

+1

Câu hỏi thực sự hay. Đầu tiên, đối với loại hồ sơ này, tôi sẽ sử dụng bản phát hành/Bảng điều khiển - vì vậy, 'Debug.WriteLine' trông không đúng chỗ; nhưng ngay cả với 'Console.WriteLine' số liệu thống kê của tôi là tương tự: DynamicMethod: 630 ms Lambda: 561 ms Phương pháp: 553 ms Biểu hiện: 360 ms Tôi vẫn đang tìm kiếm ... –

Trả lời

53

Phương pháp tạo thông qua DynamicMethod đi qua hai thunks, trong khi các phương pháp tạo ra thông qua Expression<> không đi qua bất kỳ.

Đây là cách hoạt động. Dưới đây là trình tự gọi điện thoại để gọi fn(0, 1) trong phương pháp Time (I mã hóa cứng các đối số 0 và 1 để dễ gỡ lỗi):

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 

Đối với gọi đầu tiên tôi điều tra, DynamicMethod, dòng call eax đi lên như như vậy:

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] 

Điều này có vẻ như đang thực hiện một số hành động lộn xộn để sắp xếp lại đối số. Tôi suy đoán rằng đó là do sự khác biệt giữa các đại biểu sử dụng đối số tiềm ẩn 'này' và những người không làm điều đó.

Đó nhảy cuối cùng giải quyết như sau:

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

Phần còn lại của mã tại 0098c098 trông giống như một thunk JIT, mà bắt đầu đã được viết lại với một jmp sau JIT. Đó là chỉ sau khi nhảy này mà chúng ta có được mã thực:

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 

Trình tự gọi cho phương pháp tạo ra thông qua Expression<> là khác nhau - đó là thiếu mã chồng swizzling. Đây là, từ bước nhảy đầu tiên qua 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 

Bây giờ, mọi thứ diễn ra như thế nào?

  1. stack swizzling là không cần thiết (đối số đầu tiên tiềm ẩn từ các đại biểu được thực tế sử dụng, tức là không giống như một đại biểu bị ràng buộc vào một phương pháp tĩnh)
  2. Các JIT phải được buộc bởi LINQ biên soạn logic để đại biểu đã tổ chức địa chỉ đích thực chứ không phải địa chỉ giả mạo.

Tôi không biết làm thế nào LINQ buộc JIT, nhưng tôi biết làm thế nào để buộc một JIT bản thân mình - bằng cách gọi hàm ít nhất một lần. CẬP NHẬT: Tôi tìm thấy một cách khác để buộc một JIT: sử dụng các restrictedSkipVisibility argumetn để các nhà xây dựng và vượt qua true. Vì vậy, đây là mã sửa đổi mà loại bỏ đống swizzling bằng công ngầm tham số 'này', và sử dụng các nhà xây dựng thay thế cho pre-biên dịch để các địa chỉ bị ràng buộc là địa chỉ thực, chứ không phải là 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; 
     } 
    } 
} 

Dưới đây là các runtimes trên hệ thống của tôi:

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

CẬP NHẬT ĐẾN ADD:

tôi đã cố gắng chạy mã này trên hệ thống mới của tôi, đó là một Core i7 920 chạy Windows 7 x64 với .NET 4 beta 2 được cài đặt (m scoree.dll ver. 4.0.30902), và kết quả là, tốt, biến.

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ó lẽ đây là Intel SpeedStep ảnh hưởng đến kết quả hoặc có thể là Turbo Boost. Trong mọi trường hợp, nó rất khó chịu.

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 

Nhiều kết quả này sẽ gây ra sự cố về thời gian, bất kỳ điều gì gây ra sự tăng tốc ngẫu nhiên trong kịch bản C# 3.5/runtime v2.0. Tôi sẽ phải khởi động lại để xem liệu SpeedStep hoặc Turbo Boost có chịu trách nhiệm cho những hiệu ứng này hay không.

+0

Vì vậy, điều đó có nghĩa là tôi cần phải thêm một cách để gọi phương thức của mình một cách an toàn, chỉ để tăng hiệu suất đó? Tôi chắc chắn có thể làm điều đó. –

+1

Ý của tôi là ... các phương pháp tôi tạo ra sẽ không thực sự tổng hợp hai con số, nhưng phải chịu trách nhiệm xây dựng và giải quyết các dịch vụ trong một triển khai IoC. Trong trường hợp này, tôi không thực sự muốn phương thức đầy đủ để thực thi và xây dựng một dịch vụ, chỉ để có được hiệu suất nhỏ đó. Nhìn thấy như một số dịch vụ sẽ được sử dụng * rất nhiều *, và dịch vụ thực tế là rất nhỏ và nhẹ, tôi cũng đang nỗ lực đưa vào mã độ phân giải thực tế. Bên cạnh đó, nó là một dự án học tập thú vị cho reflection.emit. Thực sự đánh giá cao công việc bạn đưa vào câu trả lời! –

+4

Một phân tích hấp dẫn và chuyên sâu. Cảm ơn –