paint-brush
[Tutorial] Give Your .NET Object A Shot Of Lifelineby@anand-gupta

[Tutorial] Give Your .NET Object A Shot Of Lifeline

by Anand GuptaJuly 20th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In my previous post, I discussed the eager root collection as an aggressive behavior of JIT (in Release mode / optimized code) to assist the garbage collector (GC), so that a object is not considered to be a root beyond the point of its usage.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - [Tutorial] Give Your .NET Object A Shot Of Lifeline
Anand Gupta HackerNoon profile picture

In my previous post, I discussed the eager root collection as an aggressive behavior of JIT (in Release mode / optimized code) to assist the garbage collector (GC), so that a object is not considered to be a root beyond the point of its usage.

But sometimes we may want to extend the lifetime of an object beyond its usual lifetime. In this post, we will see an example of a scenario where we may want to extend the lifetime of an object. Also, we will look at how we can accomplish this in two ways — one is writing an API ourselves (in the spirit of learning by doing) and other is to use existing API in .NET.

Consider the below code to display the current time using a timer endlessly till we decide to exit the program:

using System;
using System.Runtime.CompilerServices;

class Program
{
 static void Main(string[] args)
   {
       var timer = new Timer((state) => Console.WriteLine($"Time is {DateTime.Now.ToShortTimeString()}"), null, 0, 200);
       GC.Collect();
       GC.WaitForPendingFinalizers();
       GC.Collect();
  
       Console.ReadLine();
   }
}

Try running this under Debug mode, you will see an output as below. I hope you are not surprised by this result. You can see the output prints the time endlessly until we exit the program.

Time is 7:03 PM
Time is 7:03 PM
Time is 7:03 PM
Time is 7:03 PM
Time is 7:03 PM
Time is 7:03 PM
…….

Now try running the same code under Release mode.

The output when we run the same code under Release mode is nothing. The timer does not fire at all (or may only fire once or twice depending on the timing between execution garbage collection and timer firing).

Surprised! (or may be not if you know about eager root collection or have already read my previous blog on eager root collection)

This happens because of eager root collection. The timer object is not being used after line 8, hence when we initiate the garbage collection from line 9 to line 11, timer object is not reported as live roots by JIT, hence garbage collector does not mark it during mark phase and gets collected by it during sweep phase.

So, we have a situation where our code works as expected (timer fires endlessly till program exit) in Debug mode, but does not work (timer does not fire) at all in the Release mode. This is the quintessential “it works on my machine” moment!

The obvious question is what can I do about it? How can I ensure my code works as expected in both the Debug mode and Release mode? We like the idea of surprises (I know my wife does!), but we don’t like the unpleasant ones and most definitely don’t like it in production systems.

The solution is to add just a single line of code in our Main() method using the API in .NET that will give our humble timer object the much needed lifeline, so it can live longer and keep firing to show us the time till we want (but within the lifetime of the method execution). But before I come to that, let us try and write my own version of it - armed with our knowledge of eager root collection and method inlining.

We will design our API as a static class called Lifeline with a static method called KeepAlive().

Here is the full source code of my Lifeline API.

using System.Runtime.CompilerServices;

 public static class Lifeline
  {
      [MethodImpl(MethodImplOptions.NoInlining)]
      public static void KeepAlive(object obj) { }
  }

Yes, you seeing it right. The Lifeline.KeepAlive() method has absolutely no code in it. We are not using the obj argument we are being sent by the caller of this method. The key feature of this method is the attribute on the method [MethodImpl(MethodImplOptions.NoInlining)]This guarantees the JIT will never try to inline this method. What this means is, the call to this method will never be ‘inlined’, and a call to this method will definitely be made by passing the argument to its method parameter of type object (the design choice to have object type in the parameter is deliberate, so that this method can be invoked for any type of object).

Now we will invoke the Lifeline.KeepAlive() API method from the Main() method of our code as below and run it under Release mode (as well as Debug mode). Please note the call to Lifetime.KeepAlive() at line 14 by passing timer object as the argument :

using System;
using System.Runtime.CompilerServices;

class Program
{
    static void Main(string[] args)
    {
        var timer = new Timer((state) => Console.WriteLine($"Time is {DateTime.Now.ToShortTimeString()}"), null, 0, 200);
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        Console.ReadLine();
        Lifeline.KeepAlive(timer); // This will extend the lifetime of the timer object till this line of code        
    }
 }
 
 public static class Lifeline
  {

      [MethodImpl(MethodImplOptions.NoInlining)]
      public static void KeepAlive(object obj) { }
  }

Hurray! We could extend the lifetime of an object with a single small dose of Lifeline.KeepAlive().

Time is 7:45 PM
Time is 7:45 PM
Time is 7:45 PM
Time is 7:45 PM
Time is 7:45 PM
Time is 7:45 PM
…….

As promised, let us now discuss how we can accomplish this using existing .NET API. You can save yourself the effort of writing a Lifeline API as I did above and simply call GC.KeepAlive() provided in .NET. KeepAlive() is a static method on .NET framework’s (and .NET Core) GC class.

You might imagine GC.KeepAlive() must implement a very complicated and sophisticated logic — after all it is extending the lifetime of an object and messing with the default Release mode JIT behavior. But you may surprised to note, the implementation of GC.KeepAlive() is exactly identical to Lifeline.KeepAlive() we wrote — GC.KeepAlive() does not do anything, its method is completely empty and does nothing with the object we pass to it. It uses the power of [MethodImpl(MethodImplOptions.NoInlining)] to prevent JIT from inlining itI encourage you to take a look at the implementation of GC.KeepAlive() in .NET framework source code.

We can change our code to use the GC.KeepAlive() .NET API to extend the lifetime of any object till the point of invocation of GC.KeepAlive(). Please note the call to the GC .KeepAlive() at line 14:

 using System;
 
 class Program
  {
      static void Main(string[] args)
      {
          var timer = new Timer((state) => Console.WriteLine($"Time is {DateTime.Now.ToShortTimeString()}"), null, 0, 200);
          GC.Collect();
          GC.WaitForPendingFinalizers();
          GC.Collect();

          Console.ReadLine();

          GC.KeepAlive(timer); // This will extend the lifetime of timer object till this line of code
      }
   }

The lifetime of the timer object has been extended till the point of invocation of GC.KeepAlive() on line 14.

Digging Deeper — for the more curious souls

We can use the WinDbg tool (in conjunction with SOS extension) to further examine and verify the JIT behavior and GC roots in Release mode with and without GC.KeepAlive().

If we run the code in Release mode, but without the GC.KeepAlive(), and examine the GCInfo (command !gcinfo on line 11), we will observe that there are no stack roots and also there are no instances of Timer in the heap (indicated by no System.Threading.Timer entry when we execute !dumpheap command on line 43).

0:000> .load C:\Users\anand\.dotnet\sos\sos.dll

0:000> !name2ee MyPlaygroundCore MyPlaygroundCore.Program.Main
Module:      00007ff9d41af820
Assembly:    MyPlaygroundCore.dll
Token:       0000000006000001
MethodDesc:  00007ff9d41b1b50
Name:        MyPlaygroundCore.Program.Main(System.String[])
JITTED Code Address: 00007ff9d4110e90

0:000> !gcinfo 00007ff9d4110e90
entry point 00007FF9D4110E90
Normal JIT generated code
GC info 00007FF9D41E4978
Pointer table:
Prolog size: 0
Security object: <none>
GS cookie: <none>
PSPSym: <none>
Generics inst context: <none>
PSP slot: <none>
GenericInst slot: <none>
Varargs: 0
Frame pointer: rbp
Wants Report Only Leaf: 0
Size of parameter area: 0
Return Kind: Scalar
Code size: 1ee
00000049 is a safepoint: 
0000006a is a safepoint: 
000000bb is a safepoint: 
000000ba +rdi
000000dc is a safepoint: 
00000114 is a safepoint: 
00000127 is a safepoint: 
0000015d is a safepoint: 
00000170 is a safepoint: 
000001b0 is a safepoint: 
000001c3 is a safepoint: 
000001d0 is a safepoint: 
000001ed is a safepoint: 

0:007> !dumpheap -type System.Threading.Timer
         Address               MT     Size
000002ad1419b0b8 00007ff9d41e2e58       64     
000002ad1419b2a8 00007ff9d41e5760       88     
000002ad1419b300 00007ff9d41e56f0       80     
000002ad1419b368 00007ff9d41e56f0       80     
000002ad1419b3b8 00007ff9d41e56f0       80     
000002ad1419b408 00007ff9d41e56f0       80     
000002ad1419b458 00007ff9d41e56f0       80     
000002ad1419b4a8 00007ff9d41e56f0       80     
000002ad1419b4f8 00007ff9d41e56f0       80     
000002ad1419b548 00007ff9d41e56f0       80     
000002ad1419b8f0 00007ff9d41e8280       32     

Statistics:
              MT    Count    TotalSize Class Name
00007ff9d41e8280        1           32 System.Threading.TimerQueue+AppDomainTimerSafeHandle
00007ff9d41e2e58        1           64 System.Threading.TimerCallback
00007ff9d41e5760        1           88 System.Threading.TimerQueue[]
00007ff9d41e56f0        8          640 System.Threading.TimerQueue
Total 11 objects

As discussed, when we add GC.KeepAlive(), the timer object will be kept alive. To verify this let us examine using WinDbg (and SOS.dll). We can observe there is a live root in stack at stack position rbp-40 as indicated by the result of !gcinfo on line 11. This can be further confirmed by !dumpheap command on line 53 that shows the presence of 1 object in the heap of type System.Threading.Timer (line 75)and by !gcroot command (line 83) that conclusively shows rb-40 stack location points to an object of type System.Threading.Timer (line 86 and line 87).

0:000&gt; .load C:\Users\anand\.dotnet\sos\sos.dll

0:000&gt; !name2ee MyPlaygroundCore MyPlaygroundCore.Program.Main
Module:      00007ff9d419f820
Assembly:    MyPlaygroundCore.dll
Token:       0000000006000001
MethodDesc:  00007ff9d41a1b50
Name:        MyPlaygroundCore.Program.Main(System.String[])
JITTED Code Address: 00007ff9d4100e90

0:000&gt; !gcinfo 00007ff9d4100e90
entry point 00007FF9D4100E90
Normal JIT generated code
GC info 00007FF9D41D4990
Pointer table:
Prolog size: 0
Security object: &lt;none&gt;
GS cookie: &lt;none&gt;
PSPSym: &lt;none&gt;
Generics inst context: &lt;none&gt;
PSP slot: &lt;none&gt;
GenericInst slot: &lt;none&gt;
Varargs: 0
Frame pointer: rbp
Wants Report Only Leaf: 0
Size of parameter area: 0
Return Kind: Scalar
Code size: 201
0000004c is a safepoint: 
0000006d is a safepoint: 
000000be is a safepoint: 
000000bd +rdi
000000e6 is a safepoint: 
000000e5 +rbp-40
0000011e is a safepoint: 
0000011d +rbp-40
00000131 is a safepoint: 
00000130 +rbp-40
00000167 is a safepoint: 
00000166 +rbp-40
0000017a is a safepoint: 
00000179 +rbp-40
000001ba is a safepoint: 
000001b9 +rbp-40
000001cd is a safepoint: 
000001cc +rbp-40
000001da is a safepoint: 
000001d9 +rbp-40
000001e3 is a safepoint: 
00000200 is a safepoint: 


0:000&gt; !dumpheap -type System.Threading.Timer
         Address               MT     Size
0000024306a4b0b8 00007ff9d41c2e58       64     
0000024306a4b0f8 00007ff9d41c3138       24     
0000024306a4b110 00007ff9d41c47f0       96     
0000024306a4b2a8 00007ff9d41c5760       88     
0000024306a4b300 00007ff9d41c56f0       80     
0000024306a4b368 00007ff9d41c56f0       80     
0000024306a4b3b8 00007ff9d41c56f0       80     
0000024306a4b408 00007ff9d41c56f0       80     
0000024306a4b458 00007ff9d41c56f0       80     
0000024306a4b4a8 00007ff9d41c56f0       80     
0000024306a4b4f8 00007ff9d41c56f0       80     
0000024306a4b548 00007ff9d41c56f0       80     
0000024306a4b8f0 00007ff9d41c8280       32     
0000024306a4b938 00007ff9d41c4968       24     
0000024306a4b9c0 00007ff9d41c86a8       24     

Statistics:
              MT    Count    TotalSize Class Name
00007ff9d41c86a8        1           24 System.Threading.TimerQueueTimer+&lt;&gt;c
00007ff9d41c4968        1           24 System.Threading.TimerHolder
00007ff9d41c3138        1           24 System.Threading.Timer
00007ff9d41c8280        1           32 System.Threading.TimerQueue+AppDomainTimerSafeHandle
00007ff9d41c2e58        1           64 System.Threading.TimerCallback
00007ff9d41c5760        1           88 System.Threading.TimerQueue[]
00007ff9d41c47f0        1           96 System.Threading.TimerQueueTimer
00007ff9d41c56f0        8          640 System.Threading.TimerQueue
Total 15 objects

0:000&gt; !gcroot 0000024306a4b0f8
Thread 52f8:
    0000005B161FE7B0 00007FF9D410106A MyPlaygroundCore.Program.Main(System.String[])
        rbp-40: 0000005b161fe830
            -&gt;  0000024306A4B0F8 System.Threading.Timer

Found 1 unique roots (run '!gcroot -all' to see all roots).

Conclusion

The JIT has an aggressive behavior when we run our code in Release mode (optimized code). This leads to what is known as eager root collection. Objects may get collected by GC even though they may still appear to exist from lexical point of view. But if we want, we can extend the lifetime of objects. This can be done via GC.KeepAlive() API of .NET. We also discussed how we can write our own implementation of this API to better understand and appreciate how GC.KeepAlive() achieves its objective of extending an object’s lifetime in a surprisingly trivial manner.