Friday, June 5, 2015

JIT compiler be JITing

So I'm exploring the world of IL a little bit to get a deeper understanding of the common language runtime (CLR), that .NET runs upon. When doing that, ildasm.exe and windbg.exe are the go-to tools. The first will show you the disassembled IL code, the second will show you what's really happening during execution, including the actual IL code that's being executed.

I've always heard that the JIT compiler compiles 'Just In Time', but by playing with windbg.exe, I was actually able to see that it indeed only compiles the methods that are being called, down to machine code.

Let me show you an example. Here's a super simple piece of code for a console app:

Now when you compile this, you can see the IL code using ildasm.exe. You can see the IL code for the Main() as well as the Add() method. It's actually not all that scary when you look at it:

Here's the Add() method:
.method public hidebysig static int32  Add(int32 a,
                                           int32 b) cil managed
  // Code size       9 (0x9)
  .maxstack  2
  .locals init (int32 V_0)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldarg.1
  IL_0003:  add
  IL_0004:  stloc.0
  IL_0005:  br.s       IL_0007
  IL_0007:  ldloc.0
  IL_0008:  ret
} // end of method application::Add

On line IL_0001 and IL_0002, the two method parameters are placed on a 'stack' like structure, then the 'add' method is called on it, the result is stored, loaded and returned.
This, by the way, reveils that I compiled without any optimizers. Because with the -o+ flag, the code is shorter:

.method public hidebysig static int32  Add(int32 a,
                                           int32 b) cil managed
  // Code size       4 (0x4)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  add
  IL_0003:  ret
} // end of method application::Add

No more 'nop' operators, and no more overhead for storing and loading. These 'nop' and store/load operations do have a purpose: they allow the developer to place a breakpoint on that line and inspect the variables at that point.

Now, if run app.exe, open windbg.exe and attach to the process:

Then we need to run a couple of windbg commands.
!loadby sos clr
!DumpModule -mt 00007ffcaaa040c0
!DumptMT -md 00007ffcaaa059a8

This commands should result in something like the UI below:

So the first line, loads the sons of strike plugin (sos), which contains the commands to inspect the CLR. Commands start with an exclamation mark, by the way.

Now we need to first dump the domain. I called the method twice, since there's a bug in windbg.exe (ironic) apparently. This will show us the application module, and using DumptMT, we get the method table information. Not only does this table show us the methods, it also shows which ones are JIT compiled and which aren't.

As you can see - when we hit the first Console.Readline(), the Add() method is not yet JIT compiled:

However, if we continue execution (note that you also have to continue in windbg), and we use the Add() method to calculate a result, you can see that the method is JIT compiled afterwards.

Et voila! You can see that the JIT compiler only JITs the method when it's going to run this.

If you want to know why I'm diving into these topics and where I learnt how to do it? Read this. I'm just doing what I saw Bart de Smet do on PluralSight

No comments:

Post a Comment