为什么Func&lt;&gt;从Expression <func <>&gt;创建慢于Func&lt;&gt;直接声明?</func <>

时间:2010-11-18 03:27:03

标签: c# delegates expression expression-trees func

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

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

我刚做了一点测试,从Expression创建的Func<>比“{1}}直接声明的时间”几乎加倍。

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

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

// Direct
Func<int, Foo> 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<int, Foo>> expression = x => new Foo(x * 2);
Func<int, Foo> 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; }
}

我怎样才能恢复演出?

我能做些什么来使Func<>创建的Expression<Func<>>像直接声明的那样执行?

6 个答案:

答案 0 :(得分:19)

正如其他人所提到的,调用动态委托的开销导致您的速度减慢。在我的电脑上,我的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<int, Foo>)Delegate.CreateDelegate(
                typeof(Func<int, Foo>), 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的开销。

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

答案 1 :(得分:6)

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

从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 '<Main>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::<Main>m__0

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

答案 2 :(得分:3)

最终归结为Expression<T>不是预先编译的代表。它只是一个表达式树。在LambdaExpression上调用编译(实际上是Expression<T>)会在运行时生成IL代码,并为其创建类似于DynamicMethod的内容。

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

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

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

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

  3. ...另外,在Expression<T>的秒表测试中,你应该在i = 1时启动计时器,而不是0 ...我相信你的编译Lambda在第一次调用之前不会被JIT编译,因此第一次通话会有性能受损。

答案 3 :(得分:1)

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

Func<int, Foo> func = x => new Foo(x * 2);
Expression<Func<int, Foo>> 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

这是第一个函数::

的反射器版本
   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结尾,即使是不返回任何内容的函数。

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

最终,我甚至不会担心这种差异。如果你真的在你的申请过程中调用你的功能30亿次,并且产生的差异是5秒,你可能会没事。

答案 4 :(得分:1)

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

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

如果我将代码更改为

Func<int, int> test1 = x => x * 2;

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

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

<强>更新

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

但是,我还验证了Func<int, int>类型代表的编号,它们非常相似,远低于原始代码的编号。

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

答案 5 :(得分:0)

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

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

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

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