【问题标题】:Why is Func<> created from Expression<Func<>> slower than Func<> declared directly?为什么从 Expression<Func<>> 创建的 Func<> 比直接声明的 Func<> 慢?
【发布时间】:2010-11-18 03:27:02
【问题描述】:

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

我刚刚在我正在开发的应用程序中从使用直接声明的Func&lt;IInterface, object&gt; 更改为从Expression&lt;Func&lt;IInterface, object&gt;&gt; 创建的应用程序,我注意到性能下降了。

我刚刚做了一个小测试,从表达式创建的Func&lt;&gt; 花费的时间“几乎”是直接声明的Func&lt;&gt; 的两倍。

在我的机器上,直接Func&lt;&gt; 大约需要 7.5 秒,Expression&lt;Func&lt;&gt;&gt; 大约需要 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; }
}

我怎样才能恢复性能?

我可以做些什么来让从Expression&lt;Func&lt;&gt;&gt; 创建的Func&lt;&gt; 像直接声明的那样执行?

【问题讨论】:

  • 有趣的问题;我实际上接近直接案例的 4 倍。
  • (我的时间是在发布,在命令行,两次测试之前都有完整的GC)
  • 如果 Func 是 Func 似乎没有区别
  • 反映和读出为每种机制生成的 IL 可能会很有启发性。
  • @cdhowie 我无法让 dnp 为这个构建反汇编:| dotnetpad.net/ViewPaste/_Vx1bk-DVkqxCcSU1HE8tw#

标签: c# delegates expression expression-trees func


【解决方案1】:

正如其他人所提到的,调用动态委托的开销会导致您的速度变慢。在我的计算机上,我的 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。

既然可以拥有更快的委托 (test3),为什么还要费心编译 lambda (test2)?因为通常创建动态程序集的开销要大得多,并且每次调用只为您节省 10-20ns。

【讨论】:

  • 真的很好。我很快将它包装在一个扩展方法中,我的“速度”恢复了(增加了大约 30-40%)谢谢! :)
  • 仅供参考,在 .NET 4.5 中,我衡量编译表达式和上述编译方法方法之间没有区别。
【解决方案2】:

(这不是一个正确的答案,而是旨在帮助发现答案的材料。)

从 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。

【讨论】:

  • 我担心 Mono 运行时与 .Net 运行时不够相似,因此比较有用。
  • mono 使用reflection.emit 编译c sharp,因此表达式树生成的代码同样快是有道理的。
  • @Michael:那么你是说编译后的表达式树在 Mono 上很慢,还是编译后的程序集很快?
  • @Gabe:此方法的 IL 应该足够简单,以便两个运行时都将匿名方法和表达式树编译为相同的 IL。没有比上述 IL 更优化的了。我并不是建议我从 Mono 上的表达式树中提取编译后的 IL,因为这不是这个问题的目标。但是上面的IL应该可以作为一个很好的比较参考。 (此外,Mono 是我目前可用的全部。)
  • 我已经修改了我的答案以显示两个表达式的 IL。我认为 Mono 实际上可能有一个优势,因为 C# 动态模块令牌解析器在动态方法中有很多类型时会做得更差,因为它会进行迭代,而不是单声道编译器进行某种形式的查找。不过不要引用我的话。
【解决方案3】:

归根结底,Expression&lt;T&gt; 不是预编译的委托。它只是一个表达式树。在 LambdaExpression(实际上是 Expression&lt;T&gt;)上调用 Compile 会在运行时生成 IL 代码并为其创建类似于 DynamicMethod 的内容。

如果您只是在代码中使用 Func&lt;T&gt;,它会像任何其他委托引用一样对其进行预编译。

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

  1. Expression&lt;T&gt; 编译为委托的初始编译时间。这是巨大的。如果您对每次调用都执行此操作 - 绝对不要(但事实并非如此,因为您在调用 compile 后使用的是 Stopwatch。

  2. 基本上是在您调用 Compile 之后的 DynamicMethodDynamicMethods(甚至是强类型的委托)实际上执行起来比直接调用慢。编译时解析的Func&lt;T&gt;s 是直接调用。在动态发出的 IL 和编译时发出的 IL 之间有性能比较。随机网址:http://www.codeproject.com/KB/cs/dynamicmethoddelegates.aspx?msg=1160046

...此外,在您对Expression&lt;T&gt; 的秒表测试中,您应该在 i = 1 而不是 0 时启动计时器...我相信您编译的 Lambda 在第一次调用之前不会被 JIT 编译,所以有第一次调用会影响性能。

【讨论】:

  • 虽然您对秒表的看法是正确的,但在这种情况下它是无关紧要的,因为 JIT 编译 lambda 只需要几微秒(在我的计算机上可能是 3)。
  • 是的。动态发出的方法调用速度比预编译的慢,这仍然是一个众所周知的事实。
  • 顺便说一句,这并不总是正确的!我编写了委托,实际上将它们重写为表达式,因为编译表达式的执行速度几乎快了两倍。
  • Michael B:JeffN825 是说动态方法 invoke 更慢(在我的机器上大约 12ns),而不是它们执行 更慢。也就是说,函数调用开销更高。
  • @Jeff:实际的Func(不是 lambda)在第一次调用之前也不会被 JIT 编译。 CLR JIT 在第一次输入方法时对其进行编译。
【解决方案4】:

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

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

如果我将代码更改为

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

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

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

更新

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

但是,我还验证了 Func&lt;int, int&gt; 类型的代表的数字,它们非常相似,并且远低于原始代码的数字。

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

【讨论】:

  • 感谢您的回复。我也注意到了这种行为,并将其写为对我自己问题的评论。鳍博客 iøvrigt :)
  • 我将Foo 从一个类更改为一个结构,并注意到两个选项的时间减少了 1 秒,但其他方面的相对差异并没有减少。我怀疑你可能没有衡量你认为自己是什么。
  • @Gabe:我从问题中获取了代码并更改了声明,如我的答案所示。我还确保每个代表在测量时间之前都被叫过一次。我将尝试使用结构并更新我的答案。
  • 我认为Func&lt;int,int&gt; 的数字较低,因为 JIT 编译器可以进行一些并非适用于所有情况的优化(内联、注册)。
  • @Gabe:很可能就是这样。下一步是比较两者之间的JIT编译代码。
【解决方案5】:

这很可能是因为代码的第一次调用没有被 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 那条路径,调用一条较慢的路径。 vs 中的 C# 编译器也可能发出不同的调用约定,MethodAttributes 可能会作为抖动提示执行不同的优化。

最终,我什至不会担心这种差异。如果你真的在你的应用程序过程中调用你的函数 30 亿次,并且产生的差异是 5 整秒,你可能会没事的。

【讨论】:

  • 你是说JIT编译一个包含5条指令的函数需要几秒钟?
  • 在我不应该真正关心微小差异的问题上,我同意你的看法。但是,当您编写一个将根据其性能来判断的软件时,像这样的测试用例将用于衡量它与其他竞争对手的对比,当您严重依赖代表并且您突然发现性能下降 30-40 时,这确实很重要% 与直接方法相比。幸运的是,获得表达式使我有可能优化 lambda 中发生的事情,并使其比直接方法更快。
  • 向高层推销的更重要的一点是,Expression 方法允许您比手动实现模式和委托更容易实现。因此,尽管我们都同意手动调整的 C# 可能更好,但如果您需要为每种类型或更糟糕的实例手动编写无数案例的特定代码将是一项巨大的开发工作,因为您可以编写相当优化的表达式树可以动态生成代码以在运行时调整您的行为。
【解决方案6】:

我对 Michael B. 的回答很感兴趣,所以我在秒表开始之前在每种情况下都添加了额外的电话。在调试模式下,编译(案例 2)方法快了近两倍(6 秒到 10 秒),在发布模式下,两个版本都差不多(差异约为 0.2 秒)。

现在,令我震惊的是,将 JIT 排除在等式之外,我得到的结果与 Martin 截然相反。

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

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

【讨论】:

    猜你喜欢
    • 2012-02-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-02
    • 2010-10-22
    相关资源
    最近更新 更多