2009-08-18 5 views
52

저는 주로 재미와 학습을 위해 최근에 몇 가지 최적화를하고 있는데, 몇 가지 질문이 떠오른 것을 발견했습니다.호기심 : 컴파일시 Expression <...>이 최소 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 버전은 (아마도 Windows 및 다른 프로그램에 의한 변화) 지속적으로 약 30-50% 느립니다 : 내 시스템에서 출력됩니다. 누구든지 그 이유를 아나? Expression<> 통해 생성 방법은 어떤을 통과하지 않는 동안,

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 –

+0

나는 핑을해야한다고 생각했다. JIT를 한 번 호출하지 않아도 JIT를 강제 실행하는 방법을 알았습니다. 'restrictedSkipVisibility' DynamicMethod 생성자 인자를 사용하십시오. 컨텍스트 (코드 보안)에 따라 사용 가능하지 않을 수 있습니다. –

+1

정말 좋은 질문입니다. 첫째,이 유형의 프로파일 링을 위해 나는 릴리즈/콘솔을 사용할 것입니다 - 그래서'Debug.WriteLine'은 제자리에서 보이지 않습니다; 하지만 Console.WriteLine이라도 내 통계는 비슷합니다 : DynamicMethod : 630 ms 람다 : 561 ms 방법 : 553 ms Expression : 360 ms 아직 찾고 있어요 ... –

답변

53

DynamicMethod를 통해 생성하는 방법은 두 썽크 진행됩니다

여기에 프로그램입니다.

다음은 작동 방식입니다. 여기 Time 방법에 fn(0, 1)를 호출하는 호출 순서입니다 (I 디버깅의 편의를 위해 0과 1로 인수를 하드 코딩) : 나는, DynamicMethod을 조사한 최초의 호출을위한

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 

call 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] 

이것은 인자를 재정렬하기 위해 약간의 swizzling을하고있는 것으로 보입니다. 나는 암묵적인 '이'주장을 사용하는 대의원과 그렇지 않은 대의원 사이의 차이로 인한 것이라고 추측한다. 마지막에 점프

은과 같이 해결 : 0098c098에서 코드의

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

나머지는 그 시작 JIT 후 jmp로 다시당했습니다 JIT 썽크처럼 보인다.

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을 전달하십시오. 그래서, 여기에 암시 적 '이'매개 변수를 사용하여 스택 스위 즐링을 제거 수정 된 코드는, 그리고 오히려 썽크보다 바운드 주소가 실제 주소가되도록 사전 컴파일 대체 생성자를 사용

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 

를 추가하는 업데이트 : 내 시스템의 런타임

내가 설치 .NET 4 베타 2와 윈도우 7의 x64를 실행하는 코어 i7 920 내 새로운 시스템에이 코드를 실행 시도

(엠 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 또는 터보 부스트 일 수 있습니다. 어쨌든, 그것은 매우 성가신 일입니다. 이러한 결과

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/런타임 버전 2.0 시나리오에서 임의의 속도 증가의 원인이 무엇이든 타이밍의 사고가 될 것입니다. SpeedStep 또는 Turbo Boost가 이러한 영향을 담당하는지 재부팅해야합니다.

+0

그렇다면 성능 향상을 위해 안전하게 메서드를 호출 할 방법을 추가해야한다는 의미입니까? 나는 확실히 그것을 할 수있다. –

+1

내가 말하는 것은 ... 실제로 만드는 방법은 두 개의 숫자를 합산하는 것이 아니라 IoC 구현에서 서비스를 구성하고 해결하는 데 책임 져야한다는 것입니다. 이 경우 전체 성능이 약간의 성능 향상을 이루기 위해 서비스를 실행하고 구성하는 데 전체 메소드가 실제로 필요한 것은 아닙니다. 일부 서비스는 * 많이 사용 * 될 것으로 보이며 실제 서비스는 아주 작고 가볍습니다. 실제 해결 코드에도 약간의 노력을 기울이고 있습니다. 게다가, 그것은 reflection.emit을위한 재미있는 학습 프로젝트입니다. 답변에 넣은 작업에 정말 감사드립니다! –

+4

매혹적인 심층 분석. 감사합니다 –