.NET – How can debug=true extend the life time of local variable

I am sure there are quite a few blog posts that advocate on not set debug=true in production code. The reason being the optimization and performance are turned off. In this blog post I am specifically going to demonstrate how the lifetime of a variable would be extended based on this setting by the JIT Compiler. The JIT compiler explicitly extends the lifetime of the local variables until the end of the method, which might not be required because it is not being used anymore.

using System;
namespace ConsoleApplication2
 internal class Program
 private static void Main(string[] args)
 var c = new Customer {Age = 20, Name = "Ted"};
 internal class Customer
 public string Name { get; set; }
 public int Age { get; set; }

In the above sourcecode I have created a new instance of customer and I am not referring it anywhere after that. I am explicitly invoking GC.Collect so that the objects that aren’t in scope would be collected.

I have compiled this code within VS.NET using the default setting, which essentially is the debug mode. And here is the output after disassembling it, which adds the Debuggable attribute to the assembly

assembly: Debuggable(DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.EnableEditAndContinue | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.Default)]

Let’s see how this attribute affects the local variable “c” lifetime. Launched the application and then attached it to Windbg.   Loaded the sosex extension

Here is the output from !mk command

0:000> !mk

Thread 0:

*** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v4.0.30319_64\mscorlib\bc19222db4406c472d9aa1f8b6e0f470\mscorlib.ni.dll

ESP              EIP

00:U 00000000002ee758 00000000774100da ntdll!ZwRequestWaitReplyPort+0xa

01:U 00000000002ee760 00000000772c2b08 KERNEL32!ConsoleClientCallServer+0x54

02:U 00000000002ee790 00000000772f5601 KERNEL32!ReadConsoleInternal+0x1f1

03:U 00000000002ee8e0 000000007730a922 KERNEL32!ReadConsoleA+0xb2

04:U 00000000002ee9c0 00000000772d9934 KERNEL32!zzz_AsmCodeRange_End+0x8bea

05:U 00000000002eea00 000007fef34417c7 clr!DoNDirectCall__PatchGetThreadCall+0x7b

06:M 00000000002eeab0 000007fedfb034a1 DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)(+0x0 IL)(+0x0 Native)

07:M 00000000002eebd0 000007fee02af59a System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Int32, Int32 ByRef)(+0x53 IL)(+0xba Native)

08:M 00000000002eec40 000007fee02af402 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)(+0x5d IL)(+0x62 Native)

09:M 00000000002eeca0 000007fedfabe63c System.IO.StreamReader.ReadBuffer()(+0xa0 IL)(+0x5c Native)

0a:M 00000000002eecf0 000007fee0245630 System.IO.StreamReader.Read()(+0x21 IL)(+0x30 Native)

0b:M 00000000002eed30 000007fee02b7458 System.IO.TextReader+SyncTextReader.Read()(+0x0 IL)(+0x38 Native)

0c:M 00000000002eed80 000007ff001701cd *** WARNING: Unable to verify checksum for C:\Users\naveen\Documents\Visual Studio 2010\Projects\ConsoleApplication2\bin\Debug\ConsoleApplication2.exe

ConsoleApplication2.Program.Main(System.String[])(+0x24 IL)(+0xad Native) [C:\Users\naveen\Documents\Visual Studio 2010\Projects\ConsoleApplication2\Program.cs, @ 10,13]

0d:U 00000000002eede0 000007fef34810b4 clr!CallDescrWorker+0x84

0e:U 00000000002eee30 000007fef34811c9 clr!CallDescrWorkerWithHandler+0xa9

0f:U 00000000002eeeb0 000007fef3481245 clr!MethodDesc::CallDescr+0x2a1

10:U 00000000002ef0e0 000007fef3581675 clr!ClassLoader::RunMain+0x228

11:U 00000000002ef330 000007fef35817ac clr!Assembly::ExecuteMainMethod+0xac

12:U 00000000002ef5e0 000007fef3581562 clr!SystemDomain::ExecuteMainMethod+0x452

13:U 00000000002efb90 000007fef3583dd6 clr!ExecuteEXE+0x43

14:U 00000000002efbf0 000007fef3583cf3 clr!CorExeMainInternal+0xc4

15:U 00000000002efc60 000007fef3607365 clr!CorExeMain+0x15

16:U 00000000002efca0 000007fef9393309 mscoreei!CorExeMain+0x41

17:U 00000000002efcd0 000007fef9425b21 MSCOREE!CorExeMain_Exported+0x57

18:U 00000000002efd00 00000000772bf56d KERNEL32!BaseThreadInitThunk+0xd

19:U 00000000002efd30 00000000773f3281 ntdll!RtlUserThreadStart+0x1d

From the call-stack we are interested in the 0c frame. So I switch to the frame using !mframe 0c

The next command is to look at the locals using !mdv and here is the output

0:000> !mdv

Frame 0xc: (ConsoleApplication2.Program.Main(System.String[])):

[A0]:args:0x00000000026a2258 (System.String[])

[L0]:c:0x00000000026a2298 (ConsoleApplication2.Customer)

[L1]:<>g__initLocal0:0x00000000026a2298 (ConsoleApplication2.Customer)

Notice the local variable reference “c” which is of type Customer and so its details are using !mdt c

0:000> !mdt c

00000000026a2298 (ConsoleApplication2.Customer)

<Name>k__BackingField:00000000026a2278 (System.String: "Ted")

<Age>k__BackingField:0x14 (System.Int32)

This is something which usually isn’t surprising to see right.  But if I change the compilation settings to release mode then here is the locals for the same code using !mdv

0:000> !mdv

Frame 0xc: (ConsoleApplication2.Program.Main(System.String[])):



Notice the change we don’t see the variable “c” of type Customer anymore after the GC.Collect in the release mode.  The reason behind this is, in debug mode ,the JIT Compiler explicitly extended the lifetime of the local variable to the end of the method and where as in release mode it does not do it.