为什么从表达式<Func >创建的Func 比直接声明的Func 慢?

为什么通过.Compile()从Expression<Func>创建的Func比使用直接声明的Func慢得多?

我刚刚使用Func直接声明为在我正在处理的应用程序中使用Expression<Func>创建的Expression<Func> ,我注意到性能下降了。

我刚做了一点测试,从Expression创建的Func占用了Func直接声明的时间的“几乎”。

在我的机器上,Direct Func大约需要7.5秒,而Expression<Func>大约需要12.6秒。

这是我使用的测试代码(运行Net 4.0)

 // Direct Func test1 = x => new Foo(x * 2); int counter1 = 0; Stopwatch s1 = new Stopwatch(); s1.Start(); for (int i = 0; i < 300000000; i++) { counter1 += test1(i).Value; } s1.Stop(); var result1 = s1.Elapsed; // Expression . Compile() Expression<Func> expression = x => new Foo(x * 2); Func test2 = expression.Compile(); int counter2 = 0; Stopwatch s2 = new Stopwatch(); s2.Start(); for (int i = 0; i < 300000000; i++) { counter2 += test2(i).Value; } s2.Stop(); var result2 = s2.Elapsed; public class Foo { public Foo(int i) { Value = i; } public int Value { get; set; } } 

我怎样才能恢复演出?

有什么办法可以让我从Expression<Func>创建的Func执行就像直接声明一样吗?

正如其他人所提到的,调用动态委托的开销导致您的速度减慢。 在我的电脑上,我的CPU处于3GHz,开销约为12ns。 解决这个问题的方法是从已编译的程序集加载方法,如下所示:

 var ab = AppDomain.CurrentDomain.DefineDynamicAssembly( new AssemblyName("assembly"), AssemblyBuilderAccess.Run); var mod = ab.DefineDynamicModule("module"); var tb = mod.DefineType("type", TypeAttributes.Public); var mb = tb.DefineMethod( "test3", MethodAttributes.Public | MethodAttributes.Static); expression.CompileToMethod(mb); var t = tb.CreateType(); var test3 = (Func)Delegate.CreateDelegate( typeof(Func), t.GetMethod("test3")); int counter3 = 0; Stopwatch s3 = new Stopwatch(); s3.Start(); for (int i = 0; i < 300000000; i++) { counter3 += test3(i).Value; } s3.Stop(); var result3 = s3.Elapsed; 

当我添加上面的代码时, result3总是比result1高出一秒,大约1ns的开销。

那么为什么当你可以有一个更快的委托( test3 )时,甚至打扰编译的lambda( test2 )? 因为创建动态程序集通常会产生更多的开销,并且每次调用只能节省10-20ns。

(这不是一个正确的答案,但是有助于发现答案的材料。)

从Mono 2.6.7收集的统计数据 – Debian Lenny – Linux 2.6.26 i686 – 2.80GHz单核:

  Func: 00:00:23.6062578 Expression: 00:00:23.9766248 

因此,在Mono上,至少两种机制似乎都会产生等效的IL。

这是由Mono的gmcs为匿名方法生成的IL:

 // method line 6 .method private static hidebysig default class Foo '
m__0' (int32 x) cil managed { .custom instance void class [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::'.ctor'() = (01 00 00 00 ) // .... // Method begins at RVA 0x2204 // Code size 9 (0x9) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldc.i4.2 IL_0002: mul IL_0003: newobj instance void class Foo::'.ctor'(int32) IL_0008: ret } // end of method Default::
m__0

我将致力于提取表达式编译器生成的IL。

最终归结为Expression不是预编译的委托。 它只是一个表达式树。 在LambdaExpression上调用Compile(这实际上是Expression )会在运行时生成IL代码,并为它创建类似于DynamicMethod的东西。

如果你只是在代码中使用Func ,它就像任何其他委托引用一样预先编译它。

所以这里有两个缓慢的来源:

  1. Expression编译为委托的初始编译时间。 这是巨大的。 如果你为每次调用都这样做 – 肯定不会(但事实并非如此,因为你在调用编译后使用秒表)。

  2. 在你调用Compile之后,它基本上是一个DynamicMethodDynamicMethod (即使是强类型的委托)实际上比直接调用更慢。 在编译时解析的Func是直接调用。 在动态发出的IL和编译时发出的IL之间存在性能比较。 随机url: http : //www.codeproject.com/KB/cs/dynamicmethoddelegates.aspx? msg = 1160046

…另外,在你的Expression秒表测试中,你应该在i = 1时启动你的计时器,而不是0 ……我相信你的编译后的Lambda在第一次调用之前不会被JIT编译,所以会有第一次通话的性能受到影响。

仅供记录:我可以使用上面的代码重现数字。

需要注意的一点是,两个委托都为每次迭代创建一个新的Foo实例。 这可能比创建代理的方式更重要。 这不仅会导致大量的堆分配,而且GC也可能会影响这里的数量。

如果我将代码更改为

 Func test1 = x => x * 2; 

 Expression> expression = x => x * 2; Func test2 = expression.Compile(); 

性能数字几乎相同(实际上result2比result1好一点)。 这支持了这样的理论:昂贵的部分是堆分配和/或集合,而不是委托的构造方式。

UPDATE

在Gabe的评论之后,我尝试将Foo改为结构。 不幸的是,这会产生与原始代码大致相同的数字,因此也许堆分配/垃圾收集不是原因。

但是,我还validation了类型为Func委托的数字,它们非常相似,远低于原始代码的数字。

我会继续挖掘并期待看到更多/更新的答案。

这很可能是因为代码的第一次调用没有被jitted。 我决定看看IL,它们实际上是相同的。

 Func func = x => new Foo(x * 2); Expression> exp = x => new Foo(x * 2); var func2 = exp.Compile(); Array.ForEach(func.Method.GetMethodBody().GetILAsByteArray(), b => Console.WriteLine(b)); var mtype = func2.Method.GetType(); var fiOwner = mtype.GetField("m_owner", BindingFlags.Instance | BindingFlags.NonPublic); var dynMethod = fiOwner.GetValue(func2.Method) as DynamicMethod; var ilgen = dynMethod.GetILGenerator(); byte[] il = ilgen.GetType().GetMethod("BakeByteArray", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(ilgen, null) as byte[]; Console.WriteLine("Expression version"); Array.ForEach(il, b => Console.WriteLine(b)); 

这段代码为我们提供了字节数组并将它们打印到控制台。 这是我机器上的输出::

 2 24 90 115 13 0 0 6 42 Expression version 3 24 90 115 2 0 0 6 42 

这里是第一个函数的reflection器版本::

  L_0000: ldarg.0 L_0001: ldc.i4.2 L_0002: mul L_0003: newobj instance void ConsoleApplication7.Foo::.ctor(int32) L_0008: ret 

整个方法只有2个字节不同! 它们是第一个操作码,它是第一个方法,ldarg0(加载第一个参数),但是第二个方法是ldarg1(加载第二个参数)。 这里的区别是因为表达式生成对象实际上具有Closure对象的目标。 这也可以考虑。

两者的下一个操作码是ldc.i4.2(24),这意味着将2加载到堆栈上,下一个是mul (90)的操作码,下一个操作码是newobj操作码(115)。 接下来的4个字节是.ctor对象的元数据标记。 它们是不同的,因为这两种方法实际上托管在不同的程序集中。 匿名方法是匿名程序集。 不幸的是,我还没有完全弄清楚如何解决这些令牌。 最终的操作码是42,这是ret 。 每个CLI函数必须以不返回任何内容的ret even函数结束。

几乎没有可能,闭包对象在某种程度上导致事情变得更慢,这可能是真的(但不太可能),抖动没有jit方法,因为你在快速旋转连续射击它没有时间到jit那条路径,调用一条较慢的路径。 vs中的C#编译器也可能发出不同的调用约定,而MethodAttributes可以作为抖动的提示来执行不同的优化。

最终,我甚至不会担心这种差异。 如果你真的在你的应用程序中调用你的function30亿次,并且产生的差异是整整5秒,你可能会好起来的。

我对迈克尔·B的回答很感兴趣。所以我在每个案例中都添加了额外的电话,直到秒表开始。 在调试模式下,编译(案例2)方法快了近两倍(6秒到10秒),而在发布模式下,两个版本的版本都相同(差异约为0.2秒)。

现在,对我而言,令人震惊的是,在JIT推出的方程式中,我得到的结果与马丁相反。

编辑:最初我错过了Foo,所以上面的结果是针对Foo的字段,而不是属性,原始Foo的比较是相同的,只有时间更大 – 直接func为15秒,编译版本为12秒。 同样,在释放模式中,时间相似,现在差异约为0.5。

然而,这表明,如果你的表达更复杂,即使在发布模式下也会有真正的差异。