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.
.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
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
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With