为什么从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<>>
像直接声明的那样执行?
答案 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>
,它就会像任何其他委托引用一样编译它。
所以这里有两个缓慢的来源:
将Expression<T>
编译为委托的初始编译时间。这是巨大的。如果你为每次调用都这样做 - 肯定不会(但事实并非如此,因为你在调用compile之后就使用了秒表。
在您调用Compile之后,它基本上是DynamicMethod
。 DynamicMethod
s(即使是强类型的委托)实际上比直接调用更慢。在编译时解析的Func<T>
是直接调用。在动态发出的IL和编译时发出的IL之间存在性能比较。随机网址:http://www.codeproject.com/KB/cs/dynamicmethoddelegates.aspx?msg=1160046
...另外,在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。
然而,这表明,如果你的表达更复杂,即使在发布模式下也会有真正的差异。