为什么.maxstack值在释放模式dll / exe中更多?

时间:2016-11-03 11:02:41

标签: c# cil

我很想知道在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

3 个答案:

答案 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)

正如您所看到的,发布模式构建避免了在不需要的地方使用本地。这意味着堆栈的最大大小需要更大,但避免了许多不必要的ldlocstloc。总的来说,发布代码在堆栈使用方面更好地组织 - 这有助于JIT编译器更好地分析寄存器使用(请记住,IL堆栈与x86堆栈不同)。正如您所看到的,代码也相当短,节省了图像大小。

要意识到的另一个重要方面是,较大的maxstack并没有真正的性能影响 - 它更多的是关于代码的安全性。它是程序验证的一部分,而不是性能优化。这是避免堆栈溢出问题的上限 - 代码声明它需要多少堆栈空间作为最大值,并且如果有一个循环继续超出该范围,例如,运行时知道代码违反了合同并且不是'有效。在更常见的情况下,当你不匹配推送和弹出时,这是作为静态分析的一部分完成的(两者都确保堆栈在所有分支中均衡并确保堆栈不超过声明的最大值)。