我很想知道在Debug和Release模式下生成的IL代码之间的区别。我写了一个简单的代码。
using System;
namespace ConsoleApplication6
{
class Program
{
static void Main(string[] args)
{
int result = int.Parse(Console.ReadLine());
if (true)
{
Console.WriteLine("Hi There");
}
Console.WriteLine("Done");
Console.ReadLine();
}
}
}
我比较了使用IL Deassembler生成的exe。 并且在发布模式下找到.maxstack值为8,在构建模式下找到1。在这里提问之前,我搜索了一些互联网文章,发现在这里计算了为任何操作堆叠的条目数。另外,根据我的理解,发布模式代码是一种更有条理和优化的方式。 有人请确认理解并告诉我,如果我错了吗?另外,我想知道,如果Release模式输出是优化的,为什么堆栈大小会增加?堆栈大小表示什么。感谢。
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 38 (0x26)
.maxstack 8
IL_0000: call string [mscorlib]System.Console::ReadLine()
IL_0005: call int32 [mscorlib]System.Int32::Parse(string)
IL_000a: pop
IL_000b: ldstr "Hi There"
IL_0010: call void [mscorlib]System.Console::WriteLine(string)
IL_0015: ldstr "Done"
IL_001a: call void [mscorlib]System.Console::WriteLine(string)
IL_001f: call string [mscorlib]System.Console::ReadLine()
IL_0024: pop
IL_0025: ret
} // end of method Program::Main
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 45 (0x2d)
.maxstack 1
.locals init ([0] int32 result,
[1] bool CS$4$0000)
IL_0000: nop
IL_0001: call string [mscorlib]System.Console::ReadLine()
IL_0006: call int32 [mscorlib]System.Int32::Parse(string)
IL_000b: stloc.0
IL_000c: ldc.i4.0
IL_000d: stloc.1
IL_000e: nop
IL_000f: ldstr "Hi There"
IL_0014: call void [mscorlib]System.Console::WriteLine(string)
IL_0019: nop
IL_001a: nop
IL_001b: ldstr "Done"
IL_0020: call void [mscorlib]System.Console::WriteLine(string)
IL_0025: nop
IL_0026: call string [mscorlib]System.Console::ReadLine()
IL_002b: pop
IL_002c: ret
} // end of method Program::Main
答案 0 :(得分:7)
编译器可以选择的方法头有两种不同版本,Fat
标头或Tiny
标头(在ECMA-335 Partition II中定义,第25.4.3和25.4节) .2尊重。)
虽然fat标头长度为12个字节,但是小标头只有一个字节。它可以通过将IL的大小限制为63字节,不支持本地或异常处理程序以及假设.maxstack
为8 来实现这一点。
由于您的调试版本使用了本地版,因此它不符合微型标题,但是您的版本构建优化了它们,允许它使用微小的标头并获得假定的.maxstack
为8而不是更小的提供.maxstack
。
答案 1 :(得分:3)
不容易看出它来自哪里,传统的C#编译器和Roslyn都没有这样做。它实际上与Release版本无关,你可以通过添加:
来看到static void Foo() {
// Nothing
}
产生:
.method private hidebysig static void Foo() cil managed
{
// Code size 2 (0x2)
.maxstack 8
IL_0000: nop
IL_0001: ret
} // end of method Program::Foo
它在Debug和Release版本中都使用.maxstack 8
。
请注意示例程序中的细微之处,C#编译器可以自行发现result
变量并未在任何地方使用,并且知道如何消除它。只需将其中一个WriteLine方法调用更改为Console.WriteLine(result)
,您就会看到.maxstack现在按预期更改为1。它只在使用/ optimize选项运行时执行此操作,这就是为什么它看起来像Release版本与它有关。
因此,诊断是.maxstack 0
始终更改为.maxstack 8
。这很像Q + D错误修复,很可能是很久以前犯下的。可能与实际上必须使用堆栈的抖动有关,以跟踪方法的返回值但该值未被使用。就像它没有在您的示例程序中。很难看出这种情况发生在哪里,我认为它发生在元数据导入器中。没有我知道的源代码,或者我还没有找到它,所以猜测很难验证。
实际上并不重要,当编译方法时,抖动的当前版本总是使用16的内部堆栈,当.maxstack时,它们只与C ++ new
运算符分配更大的堆栈值大于16。
更新:Brian的回答是正确的。不是Q + D修复,它是装配IL的微优化。它有很多一般的。该方法以较小的结构发射到组件中,省略了堆栈大小。 CLR在加载时默认为8。
答案 2 :(得分:0)
正如您所看到的,发布模式构建避免了在不需要的地方使用本地。这意味着堆栈的最大大小需要更大,但避免了许多不必要的ldloc
和stloc
。总的来说,发布代码在堆栈使用方面更好地组织 - 这有助于JIT编译器更好地分析寄存器使用(请记住,IL堆栈与x86堆栈不同)。正如您所看到的,代码也相当短,节省了图像大小。
要意识到的另一个重要方面是,较大的maxstack
并没有真正的性能影响 - 它更多的是关于代码的安全性。它是程序验证的一部分,而不是性能优化。这是避免堆栈溢出问题的上限 - 代码声明它需要多少堆栈空间作为最大值,并且如果有一个循环继续超出该范围,例如,运行时知道代码违反了合同并且不是'有效。在更常见的情况下,当你不匹配推送和弹出时,这是作为静态分析的一部分完成的(两者都确保堆栈在所有分支中均衡并确保堆栈不超过声明的最大值)。