KoP
KoP

Reputation: 86

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

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

Upvotes: 5

Views: 319

Answers (3)

Hans Passant
Hans Passant

Reputation: 942020

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.

Upvotes: 3

Brian Reichle
Brian Reichle

Reputation: 2856

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.

Upvotes: 8

Luaan
Luaan

Reputation: 63772

As you can see, the release mode build avoids using locals where not necessary. This means that the maximum size of the stack needs to be larger, but avoids many unnecessary ldlocs and stlocs. Overall, the release code is much better organized in its stack usage - which helps the JIT compiler analyze register usage better (remember, IL stack isn't the same thing as x86 stack). As you can also see, the code is also quite a bit shorter, saving on image size.

Another important bit to realize is that bigger maxstack doesn't really have any performance implications - it's really more about safety of the code. It's part of program verification, not a performance optimization. It is an upper limit to avoid stack overflow issues - the code declares how much stack space it needs as a maximum, and if there's a loop that continues to push beyond that, for example, the runtime knows the code broke the contract and isn't valid. And in the more usual case, when you mismatched pushes and pops, this is done as part of a static analysis (both making sure the stack is balanced in all branches and making sure the stack doesn't exceed the declared maximum).

Upvotes: 0

Related Questions