Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why .maxstack value is more in released mode dll/exe?

Tags:

c#

cil

I am curious to learn the difference between the IL code generated in Debug and Release mode. I have written a simple code.

        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();
                }
            }
        }

I compared the exe generated using IL Deassembler. And find .maxstack value is 8 in Release mode and 1 in Build Mode. Before asking question here, I searched some internet articles and found that the number of entries made to stack for any operation is counted here. Also, as per my understanding, Release Mode code is a way more organised and optimized one. Would anyone please confirm the understanding and let me know if I am wrong? Also, I wanted to know, if Release mode output is an optimized one, why stack size is increased? What does stack size signify. Thanks.


Below code is from Release Mode

        .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

Below code is from Build Mode

            .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
like image 920
KoP Avatar asked Nov 03 '16 11:11

KoP


2 Answers

There are two different versions of the method header the that the compiler can select from, the Fat header or the Tiny header (defined in ECMA-335 Partition II, sections 25.4.3 and 25.4.2 respectfully.)

While the fat header is 12 bytes long, the tiny header is only one byte. It can get this small by limiting the size of the IL to 63 bytes, not supporting locals or exception handlers and by assuming a .maxstack of 8.

Since your debug build uses locals, it doesn't qualify for the tiny header, but your release build optimized them out allowing it to use the tiny header and get the assumed .maxstack of 8 rather than a smaller explicitly provided .maxstack.

like image 176
Brian Reichle Avatar answered Nov 15 '22 00:11

Brian Reichle


Not that easy to see where this comes from, neither the legacy C# compiler nor Roslyn do this. It doesn't actually have anything to do with a Release build, something you can see by adding:

static void Foo() {
    // Nothing
}

Which produces:

.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

It uses .maxstack 8 both in the Debug and the Release build.

Note the subtlety in your sample program, the C# compiler can discover by itself that the result variable isn't used anywhere and knows how to eliminate it. Just change one of the WriteLine method calls to Console.WriteLine(result) and you'll see the .maxstack now changes to 1 as expected. It only does this when it is run with the /optimize option, that's why it looked like the Release build had something to do with it.

So the diagnostic is that a .maxstack 0 is always changed to .maxstack 8. This smells very strongly like a Q+D bug fix, probably committed a very long time ago. Possibly related to the jitter actually having to use stack anyway to track the return value of a method but that value not getting used. Like it didn't in your sample program. Hard to see where this happens, I think it occurs in the metadata importer. No source code is available for it that I know of, or I haven't found it yet, so that guess is hard to verify.

It doesn't actually matter, the current version of the jitters always use an internal stack of 16 when they compile a method, they only allocate a bigger stack with the C++ new operator when the .maxstack value is larger than 16.

UPDATE: Brian's answer is correct. Not a Q+D fix, it is a micro-optimization in the IL of the assembly. It has a lot of those in general. The method is emitted into the assembly with a smaller structure, it omits the stack size. The CLR defaults it to 8 when it loads it.

like image 25
Hans Passant Avatar answered Nov 15 '22 01:11

Hans Passant