好奇心:为什么expression式编译时运行速度比最小的DynamicMethod快?
我目前正在做一些最后的措施优化,主要是为了好玩和学习,并发现了一些让我留下几个问题的东西。
一,问题:
- 当我通过使用DynamicMethod在内存中构造一个方法,并使用debugging器时,在反汇编视图中查看代码时,有什么办法让我进入生成的汇编代码? debugging器似乎只是对我整个方法
- 或者,如果这是不可能的,是否有可能以某种方式将生成的IL代码作为程序集保存到磁盘,以便我可以使用Reflector对其进行检查?
- 为什么我的简单添加方法(Int32 + Int32 => Int32)的
Expression<...>
版本比最小的DynamicMethod版本运行得更快?
这是一个简短而完整的程序,演示。 在我的系统上,输出是:
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; } } }
通过DynamicMethod
创build的方法通过两个thunk,而通过Expression<>
创build的方法不经过任何。
这是如何工作的。 下面是在Time
方法中调用fn(0, 1)
的调用序列fn(0, 1)
为了便于debugging,我将参数硬编码为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
对于我调查的第一个调用DynamicMethod
, 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]
这似乎正在做一些堆栈调整参数重新排列。 我推测这是由于使用隐含的“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<>
创build的方法的调用顺序是不同的 – 它缺less堆栈debugging代码。 在这里,从第一次跳转到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
现在,事情是如何得到这样的?
- 堆栈调整是没有必要的(从委托隐含的第一个参数是实际使用,即不像一个委托绑定到一个静态方法)
- JIT必须被LINQ编译逻辑所强制,以便委托人保持真正的目标地址而不是假目标地址。
我不知道LINQ如何强制JIT,但是我知道如何强制JIT自己 – 通过调用函数至less一次。 更新:我发现另一种强制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 / runtime v2.0场景中导致随机加速。 我将不得不重新启动以查看SpeedStep或Turbo Boost是否对这些影响负责。