好奇心:为什么Expression< ...>编译时运行速度比最小的DynamicMethod快?

时间:2009-08-18 21:39:12

标签: c# profiling reflection.emit expression dynamicmethod

我目前正在做一些最后的测量优化,主要是为了娱乐和学习,并发现了一些让我有几个问题的东西。

首先,问题:

  1. 当我通过使用DynamicMethod在内存中构造方法并使用调试器时,在反汇编视图中查看代码时,有没有办法让我进入生成的汇编代码?调试器似乎只是为我提供了整个方法
  2. 或者,如果那是不可能的,我可以以某种方式将生成的IL代码作为程序集保存到磁盘,以便我可以使用Reflector进行检查吗?
  3. 为什么我的简单加法方法(Int32 + Int32 =&gt; Int32)的Expression<...>版本比最小的DynamicMethod版本运行得更快?
  4. 这是一个简短而完整的程序。在我的系统上,输出是:

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

    我希望lambda和方法调用具有更高的值,但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 个答案:

答案 0 :(得分:54)

通过DynamicMethod创建的方法经历了两次thunks,而通过Expression<>创建的方法不会通过任何方法。

这是它的工作原理。这是在fn(0, 1)方法中调用Time的调用序列(为了便于调试,我将参数硬编码为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'参数的代表与不使用隐式'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编译逻辑强制执行,以便委托保留真实的目标地址而不是伪造的地址。
  3. 我不知道LINQ如何强制JIT,但我确实知道如何自己强制使用JIT - 至少调用一次该函数。更新:我找到了强制JIT的另一种方法:使用restrictedSkipVisibility argumetn到构造函数并传递true。所以,这里的修改代码通过使用隐式'this'参数来消除堆栈混合,并使用备用构造函数进行预编译,以便绑定地址是真实地址,而不是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
    

    最新添加

    我尝试在我的新系统上运行此代码,这是运行安装了.NET 4 beta 2的Windows 7 x64的Core i7 920(mscoree.dll版本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 /运行时v2.0场景中的随机加速。我必须重新启动,看看SpeedStep或Turbo Boost是否对这些影响负责。