Inspecting Local Root Lifetime

August 25, 2010

no comments

The .NET GC determines which objects are reachable by constructing a graph of references, starting from a set of roots. One type of roots is local references that are stored on thread stacks. The GC relies on the JIT to supply it with information about the local variables in each active method, and uses that information to determine whether a root should be traversed.

When all optimizations are enabled, the JIT and GC cooperate so that local roots are relevant only as long as the local variable is still going to be used. This is a source of strange bugs—two of them are the Timer bug and the finalization race condition.

Why am I telling you all this? Well, it turns out there’s a cool SOS command called !GCInfo that can give you information on local root relevance within a method. In other words, it can tell you when (in terms of instructions) a local variable ceases to be an active local root.

For the sake of simplicity, let’s consider Debug builds only and the following method:

static void Main(string[] args)
{
    string s = "Hello World";
    Console.ReadLine();
}

Because of the Debug build, the local variable s is not going to be optimized away. Furthermore, its lifetime should extend until the end of the method, and not just the first line. Let’s verify this:

0:000> !CLRStack

OS Thread Id: 0x1f04 (0)

ESP       EIP    
[…edited for brevity…]


003ced00 704c9fbb System.IO.StreamReader.ReadBuffer()

003ced14 704c9e0c System.IO.StreamReader.ReadLine()

003ced34 70a5dd3d System.IO.TextReader+SyncTextReader.ReadLine()

003ced40 709a31c7 System.Console.ReadLine()

003ced48 005c009b FreakishTimerBug.Program.Main()

0:000> !gcinfo 005c009b

entry point 005c0070

Normal JIT generated code

GC info 001b17e0

Method info block:

    method      size   = 0031

    prolog      size   =  9

    epilog      size   =  4

    epilog     count   =  1

    epilog      end    = yes 
    callee-saved regs  = EBP

    ebp frame          = yes 
    fully interruptible= yes 
    double align       = no 
    arguments size     =  0 DWORDs

    stack frame size   =  2 DWORDs

    untracked count    =  1

    var ptr tab count  =  1

    epilog        at   002D

    argTabOffset = 4 
31 D2 B8 9A 40 |

01 04          |

Pointer table:

04             |             [EBP-04H] an untracked  local

08 1C 15       | 001C..0031  [EBP-08H] a  pointer

F3 43          | 0023        reg EAX becoming live

F0 00          | 002B        reg EAX becoming dead

FF             |

0:000> !u 005c009b

Normal JIT generated code

FreakishTimerBug.Program.Main(System.String[])

Begin 005c0070, size 31

[…edited for brevity…]

005c0070 push    ebp

005c0071 mov     ebp,esp

005c0073 sub     esp,8

005c0076 mov     dword ptr [ebp-4],ecx

005c0079 cmp     dword ptr ds:[1B30E4h],0

005c0080 je      005c0087

005c0082 call    mscorwks!JIT_DbgIsJustMyCode (710cb329)

005c0087 xor     edx,edx

005c0089 mov     dword ptr [ebp-8],edx

005c008c nop

005c008d mov     eax,dword ptr ds:[3222030h] ("Hello World")

005c0093 mov     dword ptr [ebp-8],eax

005c0096 call    mscorlib_ni+0x6d31b8 (709a31b8) (System.Console.ReadLine(), mdToken: 060007bb)

>>> 005c009b nop

005c009c nop

005c009d mov     esp,ebp

005c009f pop     ebp

005c00a0 ret

The local variable s is stored on the stack, in EBP-8. It becomes relevant at offset 1C from the beginning of the method, which is the NOP instruction at 005c008C, and it remains relevant throughout the entire method.

If we modify the code slightly, we might expect a different outcome:

static void Main(string[] args)
{
    string s = "Hello World";
    s = null;
    Console.ReadLine();
}

In this version, the local variable is explicitly cleared before the Console.ReadLine call, so it might have an effect on the JIT’s table:

0:000> !gcinfo 004000a0

entry point 00400070

Normal JIT generated code

GC info 001917e0

Method info block:

    method      size   = 0036

    prolog      size   =  9

    epilog      size   =  4

    epilog     count   =  1

    epilog      end    = yes 
    callee-saved regs  = EBP

    ebp frame          = yes 
    fully interruptible= yes 
    double align       = no 
    arguments size     =  0 DWORDs

    stack frame size   =  2 DWORDs

    untracked count    =  1

    var ptr tab count  =  1

    epilog        at   0032

    argTabOffset = 4 
36 D2 B8 9A 40 |

01 04          |

Pointer table:

04             |             [EBP-04H] an untracked  local

08 1C 1A       | 001C..0036  [EBP-08H] a  pointer

F3 43          | 0023        reg EAX becoming live

F0 05          | 0030        reg EAX becoming dead

FF             |

0:000> !u 004000a0

Normal JIT generated code

FreakishTimerBug.Program.Main(System.String[])

Begin 00400070, size 36

[…edited for brevity…]

00400070 push    ebp

00400071 mov     ebp,esp

00400073 sub     esp,8

00400076 mov     dword ptr [ebp-4],ecx

00400079 cmp     dword ptr ds:[1930E4h],0

00400080 je      00400087

00400082 call    mscorwks!JIT_DbgIsJustMyCode (710cb329)

00400087 xor     edx,edx

00400089 mov     dword ptr [ebp-8],edx

0040008c nop

0040008d mov     eax,dword ptr ds:[35E2030h] ("Hello World")

00400093 mov     dword ptr [ebp-8],eax

00400096 xor     edx,edx

00400098 mov     dword ptr [ebp-8],edx

0040009b call    mscorlib_ni+0x6d31b8 (709a31b8) (System.Console.ReadLine(), mdToken: 060007bb)

>>> 004000a0 nop

004000a1 nop

004000a2 mov     esp,ebp

004000a4 pop     ebp

004000a5 ret

There’s no change—the EBP-8 local variable remains active throughout the end of the method. You can even see the instructions that assign 0 to it—at 00400096 and 00400098, but it doesn’t change the relevance lifetime of the local variable. And it makes sense to some extent—the local variable’s eligibility to be considered a local root isn’t affected by whether the code stores a null in it.

This whole discussion, of course, is irrelevant in the Release build. In fact, the local variable wouldn’t even be there. In order to have something to show you, I modified the code slightly:

static void Main(string[] args)
{
    int k = int.Parse(Console.ReadLine());
    float j = float.Parse(Console.ReadLine());
    string s = Console.ReadLine();
    Console.WriteLine(s + j + k);
}

…and then compiled it in the Release build, and:

0:000> !gcinfo 002c0085

entry point 002c0070

Normal JIT generated code

GC info 001e17fc

Method info block:

[…edited for brevity…]

Pointer table:

14 5D 27       | 005D..0084  [EBP-14H] a  pointer

9C 40          | 001C        call [ ESI ] argMask=00

A2 40          | 003E        call [ ESI ] argMask=00

B5 40          | 0073        call [ ESI ] argMask=00

9D 40          | 0090        call [ ESI ] argMask=00

FF             |

0:000> !u 002c0085

Normal JIT generated code

FreakishTimerBug.Program.Main(System.String[])

Begin 002c0070, size a4

[…edited for brevity…]

002c00be call    mscorlib_ni+0x6d1e38 (709a1e38) (System.Console.get_In(), mdToken: 06000772)

002c00c3 mov     ecx,eax

002c00c5 mov     eax,dword ptr [ecx]

002c00c7 call    dword ptr [eax+64h]

002c00ca mov     dword ptr [ebp-14h],eax

002c00cd mov     ecx,offset mscorlib_ni+0x26a32c (7053a32c) (MT: System.Single)

002c00d2 call    001d201c (JitHelp: CORINFO_HELP_NEWSFAST)

002c00d7 mov     esi,eax

002c00d9 mov     ecx,offset mscorlib_ni+0x272d34 (70542d34) (MT: System.Int32)

002c00de call    001d201c (JitHelp: CORINFO_HELP_NEWSFAST)

002c00e3 mov     edi,eax

002c00e5 fld     dword ptr [ebp-10h]

002c00e8 fstp    dword ptr [esi+4]

002c00eb mov     edx,esi

002c00ed mov     dword ptr [edi+4],ebx

002c00f0 push    edi

002c00f1 mov     ecx,dword ptr [ebp-14h]

002c00f4 call    mscorlib_ni+0x20cc70 (704dcc70) (System.String.Concat(System.Object, System.Object, System.Object), mdToken: 060001c7)

002c00f9 mov     esi,eax

002c00fb call    mscorlib_ni+0x22d380 (704fd380) (System.Console.get_Out(), mdToken: 06000773)

[…edited for brevity…]

Indeed, in the new method, the s variable is stored on the stack, and it becomes alive at 002C00C, immediately after the return value (from EAX) of Console.ReadLine is assigned to it. It remains alive all the way through 002C00F4, where it stored in ECX and passed to String.Concat.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>