10

Ok this is merely curiosity, serves no real world help.

I know that with expression trees you can generate MSIL on the fly just like the regular C# compiler does. Since compiler can decide optimizations, I'm tempted to ask what is the case with IL generated during Expression.Compile(). Basically two questions:

  1. Since at compile time the compiler can produce different (may be slightly) IL in debug mode and release mode, is there ever a difference in the IL generated by compiling an expression when built in debug mode and release mode?

  2. Also JIT which convert IL to native code at run time should be vastly different in both debug mode and release mode. Is this also the case with compiled expressions? Or are IL from expression trees not jitted at all?

My understanding could be flawed, correct me in case.

Note: I'm considering the cases where the debugger is detached. I'm asking about the default configuration setting that comes with "debug" and "release" in visual studio.

4

3 回答 3

12

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

这个其实有一个非常简单的答案:不。给定两个相同的 LINQ/DLR 表达式树,如果一个由运行在发布模式下的应用程序编译,另一个在调试模式下编译,则生成的 IL 将没有区别。我不确定这将如何实施。我不知道任何可靠的方法可以让代码System.Core知道您的项目正在运行调试构建或发布构建。

然而,这个答案实际上可能具有误导性。表达式编译器发出的 IL 在调试和发布版本之间可能没有区别,但在 C# 编译器发出表达式树的情况下,表达式树本身的结构可能在调试和发布模式之间有所不同。我对 LINQ/DLR 内部结构相当熟悉,但对 C# 编译器了解不多,所以我只能说在这些情况下可能存在差异(也可能没有)。

此外,在运行时将 IL 转换为本机代码的 JIT 在调试模式和发布模式下应该有很大的不同。这也是编译表达式的情况吗?还是来自表达式树的 IL 根本没有被 jitted?

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

我可以告诉你,LINQ/DLRLambdaCompiler执行的优化很少——肯定少于发布模式下的 C# 编译器;调试模式可能更接近,但我会把钱花在 C# 编译器上,稍微激进一点。LambdaCompiler通常不会尝试减少临时局部变量的使用,并且条件、比较和类型转换等操作通常会使用比您预期更多的中间局部变量。我实际上只能想到它确实执行的三个优化:

  1. 嵌套的 lambda 将尽可能内联(并且“尽可能”往往是“大部分时间”)。实际上,这可以提供很大帮助。请注意,这仅在您Invoke使用LambdaExpression; 如果您在表达式中调用已编译的委托,则它不适用。

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

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

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

通常,您应该假设编译的 LINQ/DLR 表达式产生的 IL 的优化程度远低于 C# 编译器产生的 IL。但是,生成的 IL 代码符合 JIT 优化条件,因此很难评估真实世界的性能影响,除非您实际尝试使用等效代码对其进行测量。

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


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

于 2013-10-14T18:41:44.887 回答
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()总是“优化”一起发生?

于 2013-10-14T10:45:32.227 回答
1

关于 IL

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

此外,不能保证该属性存在于程序集中,并且由于我们可以在同一进程中混合和匹配发布/调试程序集,因此实际上不可能说“这是一个调试/发布过程”。

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

此外 - 表达式树编译器几乎直接将 lambda 中的表达式转换为 IL(除了一些细微差别,例如从派生引用类型到基本引用类型的冗余向下转换),因此生成的 IL被优化为你写的表达式树。因此,IL 在 Debug/Release 构建之间不太可能有所不同,因为实际上没有 Debug/Release process这样的东西,只有一个程序集,而且如上所述,没有可靠的方法来检测它。

但是 JIT 呢?

然而,当谈到将 IL 转换为汇编程序的 JIT 时,我认为值得注意的是,如果一个进程在附加调试器的情况下启动与没有启动时,JIT(虽然不确定 .Net 核心)的行为会有所不同。尝试使用 VS 中的 F5 开始发布构建,并在它已经运行后比较调试行为与附加到它。

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

我的观点的重点是,如果调试器的存在会影响静态构建 IL 的 JITing 行为,那么它可能会影响其在 JITing动态构建 IL 时的行为,例如绑定委托,或者在这种情况下,表达式树。不过,我不确定我们能说的有多么不同。

于 2017-01-16T09:08:40.880 回答