Occassionally, I find old code concepts rattling around in my head that just don’t apply in today’s world. My most recent one was the difference between release and debug builds.
Back in VB6 days, one of the big reasons for creating a debug build was the generation of the pdb file. The pdb (portable database) file contained information about the names of items such as variables, classes and methods. Furthermore, it contained information about where these values were located in the code. Without the pdb file, all of this information was unavailable.
Imagine my surprise when I ran some code without a pdb file the other day, and I received a stack trace containing method names. With a little further thought, though, I realized it wasn’t that surprising. When disassembling a dll with ILDASM, I can see all of the method and variable names with or without a pdb file present. It turns out that the inclusion of a pdb file will allow you to trace a bug to a module and line number, but it is no longer needed for artifact names.
Release builds now contain pdb files by default. So what is the difference between the two build types? To help figure this out, I generated a very simple HelloWorld class and compiled it as both a release and a debug build. This class contained two methods.
static void Main(string[] args)
{
Console.WriteLine(SayHello("Bob"));
Console.ReadLine();
}
static string SayHello(string name)
{
return String.Format("Hello {0}!", name);
}
Opening the Main method of the release dll in ILDasm yields a pretty straight forward implementation of the code. In the decompilation, you can see the creation of the string, the loading of the parameter, the string formatting, and the return of the value.
.method private hidebysig static string
SayHello(string name) cil managed
{
// Code size 12 (0xc)
.maxstack 8
IL_0000: ldstr "Hello {0}!"
IL_0005: ldarg.0
IL_0006: call string [mscorlib]System.String::Format(string,
object)
IL_000b: ret
} // end of method Program::SayHello
The debug code looks almost exactly the same, but it includes a few differences to help assist in debugging. A local variable is initialized to hold the value of parameter so that it is available to a debugger. Also added are several nop (no operation) and a br_s (branch) method. These set points within the application on which a breakpoint can be set.
.method private hidebysig static string
SayHello(string name) cil managed
{
// Code size 17 (0x11)
.maxstack 2
.locals init ([0] string CS$1$0000)
IL_0000: nop
IL_0001: ldstr "Hello {0}!"
IL_0006: ldarg.0
IL_0007: call string [mscorlib]System.String::Format(string,
object)
IL_000c: stloc.0
IL_000d: br.s IL_000f IL_000f: ldloc.0
IL_0010: ret
} // end of method Program::SayHello
I expected that tweaking the code to call a few subroutines would at least lead to some inlining optimizations within the IL. The differences between the Debug and Release versions of the IL were similar to those above. So where were the optimizations?
The only other major difference between the IL versions are the values stored in the DebuggingModes attribute. This attribute links back to the debug compilation flags that can be set when compiling a project. Since these are the only differences in the IL, the optimizations must occur within the JIT compiler itself. Scott Hanselman has an excellent post on compiled release and debug that is well worth reading.
That would be it for today! Good luck and code safe!
MW