表达树生成的IL是否已优化?

时间:2013-10-14 07:26:02

标签: c# expression-trees compiler-optimization jit il

好的,这只是好奇心,没有现实世界的帮助。

我知道使用表达式树,您可以像常规C#编译器一样动态生成MSIL。由于编译器可以决定优化,我很想知道在Expression.Compile()期间生成IL的情况是什么。基本上有两个问题:

  1. 由于在编译时编译器可以在调试模式和释放模式中产生不同的(可能是略微的)IL,因此在构建时通过编译表达式生成的IL是否存在差异在调试模式和发布模式?

  2. 在运行时将IL转换为本机代码的JIT在调试模式和发布模式下都应该有很大的不同。编译表达式也是如此吗?或者来自表达树的IL根本没有被咬过?

  3. 我的理解可能存在缺陷,请纠正我。

    注意:我正在考虑分离调试器的情况。我问的是Visual Studio中“debug”和“release”附带的默认配置设置。

3 个答案:

答案 0 :(得分:12)

  

由于编译时编译器可以在调试模式和发布模式下产生不同的(可能是略微的)IL,因此在调试模式和发布模式下构建表达式时生成的IL是否存在差异?

这个实际上有一个非常简单的答案:没有。给定两个相同的LINQ / DLR表达式树,如果由在Release模式下运行的应用程序编译而另一个在Debug模式下编译,则生成的IL将没有区别。我不知道如何实现这一点;我不知道System.Core中的代码有任何可靠的方法来知道您的项目正在运行调试版本或发布版本。

然而,这个答案实际上可能会产生误导。表达式编译器发出的IL在调试和发布版本之间可能没有区别,但是在C#编译器发出表达式树的情况下,表达式树本身的结构可能在调试和释放模式之间不同。我对LINQ / DLR内部结构非常了解,但对C#编译器没那么熟悉,所以我只能说可能在这些情况下有所不同(并且可能没有)。< / p>

  

在运行时将IL转换为本机代码的JIT在调试模式和发布模式下都应该有很大的不同。编译表达式也是如此吗?或者来自表达树的IL根本没有被咬过吗?

对于预优化的IL与未优化的IL,JIT编译器吐出的机器代码不一定非常不同。结果可能完全相同,特别是如果唯一的差异是一些额外的临时值。我怀疑两者在更大和更复杂的方法中会有更多分歧,因为JIT通常会花费时间/精力来优化给定方法。但听起来你对编译的LINQ / DLR表达式树的质量与调试或发布模式下编译的C#代码的比较感兴趣。

我可以告诉你,LINQ / DLR LambdaCompiler执行的优化很少 - 肯定比Release模式下的C#编译器少;调试模式可能更接近,但我会把我的钱放在C#编译器上稍稍激进。 LambdaCompiler通常不会尝试减少临时本地的使用,而条件,比较和类型转换等操作通常会使用比您预期的更多的中间本地。实际上我只能想到 执行的三个优化:

  1. 嵌套的lambda将在可能的情况下内联(并且“在可能的情况下”倾向于“大部分时间”)。实际上,这可以帮助很多。请注意,这仅适用于Invoke LambdaExpression;如果在表达式中调用已编译的委托,则不适用。

  2. 至少在某些情况下,省略了不必要/冗余类型的转换。

  3. 如果在编译时知道TypeBinaryExpression(即[value] is [Type])的值,则该值可以内联为常量。

  4. 除#3外,表达式编译器不进行“基于表达式”的优化;也就是说,它不会分析表达树寻找优化机会。列表中的其他优化很少或没有关于树中其他表达式的上下文。

    通常,您应该假设编译的LINQ / DLR表达式产生的IL优于C#编译器产生的IL。但是,生成的IL代码有资格进行JIT优化,因此除非您实际尝试使用等效代码进行测量,否则很难评估实际性能影响。

    在使用表达式树编写代码时要记住的一点是,实际上,是编译器 1 。 LINQ / DLR树被设计为由一些其他编译器基础结构发出,如各种DLR语言实现。因此,可以在表达级别处理优化。如果你是一个草率的编译器并发出一堆不必要的或冗余的代码,生成的IL将更大,并且不太可能被JIT编译器积极地优化。所以要注意你构建的表达式,但不要担心太多。如果你需要高度优化的IL,你应该自己发射它。但在大多数情况下,LINQ / DLR树的表现都很好。


    1 如果您曾经想知道为什么LINQ / DLR表达式对于要求精确类型匹配如此迂腐,那是因为它们旨在用作多种语言的编译器目标,每个语言都是它可能有关于方法绑定,隐式和显式类型转换等的不同规则。因此,在手动构造LINQ / DLR树时,您必须完成编译器通常在幕后执行的工作,例如自动插入隐式转换的代码。

答案 1 :(得分:2)

平方int

我不确定这是否显示,但我想出了以下示例:

// make delegate and find length of IL:
Func<int, int> f = x => x * x;
Console.WriteLine(f.Method.GetMethodBody().GetILAsByteArray().Length);

// make expression tree
Expression<Func<int, int>> e = x => x * x;

// one approach to finding IL length
var methInf = e.Compile().Method;
var owner = (System.Reflection.Emit.DynamicMethod)methInf.GetType().GetField("m_owner", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(methInf);
Console.WriteLine(owner.GetILGenerator().ILOffset);

// another approach to finding IL length
var an = new System.Reflection.AssemblyName("myTest");
var assem = AppDomain.CurrentDomain.DefineDynamicAssembly(an, System.Reflection.Emit.AssemblyBuilderAccess.RunAndSave);
var module = assem.DefineDynamicModule("myTest");
var type = module.DefineType("myClass");
var methBuilder = type.DefineMethod("myMeth", System.Reflection.MethodAttributes.Static);
e.CompileToMethod(methBuilder);
Console.WriteLine(methBuilder.GetILGenerator().ILOffset);

结果:

在Debug配置中,编译时方法的长度为8,而发出的方法的长度为4。

在Release配置中,编译时方法的长度为4,而发出的方法的长度也为4.

IL DASM在调试模式下看到的编译时方法:

.method private hidebysig static int32  '<Main>b__0'(int32 x) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       8 (0x8)
  .maxstack  2
  .locals init ([0] int32 CS$1$0000)
  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  mul
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0006
  IL_0006:  ldloc.0
  IL_0007:  ret
}

并发布:

.method private hidebysig static int32  '<Main>b__0'(int32 x) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       4 (0x4)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  mul
  IL_0003:  ret
}

免责声明:我不确定是否可以做出任何结论(这是一个很长的“评论”),但是Compile()总是会发生 “优化”?

答案 2 :(得分:1)

关于IL

正如其他答案所指出的那样,在运行时检测调试/发布并不是真正的“事情”,因为它是由项目配置控制的编译时决策,而不是在构建的程序集中可以检测到的。运行时可以反映程序集上的AssemblyConfiguration属性,检查其Configuration属性 - 但对于.Net这么基本的东西来说,这将是一个不精确的解决方案 - 因为该字符串可以字面上是任何

此外,该属性不能保证在程序集中存在,因为我们可以在同一个进程中混合和匹配发布/调试程序集,所以几乎不可能说“这是一个调试/发布过程”。

最后,正如其他人所提到的,DEBUG != UNOPTIMISED - '可调试'程序集的概念更多地是关于约定而不是其他任何东西(反映在.Net项目的默认编译设置中) - 控制细节的约定在PDB中(顺便说一下,不存在一个),以及代码是否优化。因此,可以使用优化的调试组件,以及未经优化的发布组件,甚至是具有完整PDB信息的优化发布组件,可以像标准的“调试”组件一样进行调试。

此外 - 表达式树编译器将lambda中的表达式直接转换为IL(除了一些细微差别,例如从派生引用类型到基本引用类型的冗余向下转换),因此生成的IL < em>与您编写的表达式树一样优化。因此,调试/发布版本之间的IL不太可能不同,因为实际上没有调试/发布进程,只有一个程序集,如上所述,没有可靠的方法检测到。

但JIT怎么样?

当谈到JIT将IL翻译成汇编程序时,我认为值得注意的是JIT(虽然不确定.Net核心) 表现得很好如果一个进程是在连接调试器的情况下启动而在没有启动时启动的。尝试使用VS中的F5启动发布版本,并比较调试行为与已经运行后附加到它的调试行为。

现在,这些差异可能并非主要是由于优化(差异的很大一部分可能是确保在生成的机器代码中维护PDB信息),但您会看到更多'方法优化'消息在附加到发布过程时的堆栈跟踪中,如果有的话,在运行它时使用从头开始附加的调试器。

我的观点主要是,如果调试器的存在会影响静态构建的IL的JITing行为,那么它可能会影响其在动态JITing 时的行为构建IL,例如绑定委托,或者在本例中为表达式树。但是,我不确定我们能说出来有多么不同。

相关问题