24

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

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

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

在我的机器上,DirectFunc<>大约需要 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<>>对象像直接声明的那样执行吗?

4

6 回答 6

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-20ns。

于 2010-11-18T06:06:44.580 回答
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。

这是 Monogmcs为匿名方法生成的 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。

于 2010-11-18T04:15:59.970 回答
4

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

如果您只使用 a Func<T>in 代码,它会像任何其他委托引用一样预编译它。

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

  1. Expression<T>编译成委托的初始编译时间。这是巨大的。如果您为每次调用都这样做 - 绝对不要(但事实并非如此,因为您在调用 compile.

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

...此外,在您的秒表测试中Expression<T>,您应该在 i = 1,而不是 0 时启动计时器...我相信您编译的 Lambda 在第一次调用之前不会被 JIT 编译,因此会对性能造成影响那个第一个电话。

于 2010-11-18T04:48:15.913 回答
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. ret每个 CLI 函数都必须以不返回任何内容的偶数函数结尾。

可能性很小,闭包对象以某种方式导致事情变慢,这可能是真的(但不太可能),抖动没有使方法抖动,并且由于您正在快速旋转连续射击,因此不必花时间jit 那条路径,调用一条较慢的路径。vs 中的 C# 编译器也可能发出不同的调用约定,MethodAttributes这可能会作为抖动提示来执行不同的优化。

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

于 2010-11-18T05:04:31.937 回答
1

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

需要注意的一点是,两个委托都会为每次迭代创建一个新的 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为结构。不幸的是,这产生或多或少与原始代码相同的数字,所以也许堆分配/垃圾收集毕竟不是原因。

However, I also verified the numbers for delegates of the type Func<int, int> and they are quite similar and much lower than the numbers for the original code.

I'll keep digging and look forward to seeing more/updated answers.

于 2010-11-18T06:56:13.513 回答
0

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

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

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

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

于 2010-11-18T06:34:56.907 回答