Over a million developers have joined DZone.

How Are Methods Compiled Just-In-Time and Only Then?

DZone's Guide to

How Are Methods Compiled Just-In-Time and Only Then?

Free Resource

We all know that the first time a .NET method is called, the just-in-time (JIT) compiler is invoked to compile the method’s IL representation to the native code for the particular platform it’s running on (x86, x64 or ia64). Did you ever ask yourself how this happens?

The naive way to think about it is the following: Every time a method is called, the generated native code checks a flag that indicates whether the method has already been compiled (jitted). If it has been, the code jumps directly to the compiled method; if it hasn’t been, the code calls the JIT to perform the compilation, sets the flag and then proceeds to the compiled result.

This is a very expensive thing to do, if you think about it. The cheapest method call in .NET is the static method call, where there are no setup steps or checks required to perform the method dispatch (unlike instance method calls, virtual method calls or interface method calls which are significantly more evolved). If we had to perform a global flag check for each static method call, it would incur an unjustified performance cost that—in performance-sensitive scenarios—could more than double the cost of the call.

It’s somewhat akin to thinking about handling access to an invalid memory location via a pointer. Instead of generating code to proactively check whether the pointer points to a valid memory location, the compiler emits code to directly access memory. If the CPU detects that the virtual address is not mapped, it causes a page fault that the OS memory manager handles and decides whether an access violation is required or the page is in fact available and should be mapped to the virtual address. (The former happens when you access a NULL pointer, or any other invalid address; the latter happens, for example, when a page is paged out to disk or when a page is first accessed after being committed so its page table entries need to be built and the page itself must be taken from the MM’s zero page list.)

The same idea is employed in the JIT-compilation scenario. Instead of checking a global flag, the JIT emits a stub at the call-site so that instead of the method, the stub is automatically called (and the calling code is none the wiser about this). It is this stub that calls into the JIT, compiles the target method and transparently replaces the call-site if necessary so that the call is dispatched directly to the target method. Subsequent calls do not go through the stub—it’s a wasteful thing to do. This implies that all but the first call to a method do not incur any additional performance cost, even though the method is jitted only at the first time it’s called.

I hope there’s nothing surprising about the idea itself: Lazy evaluation is a technique too common to even give it a name. Nonetheless, it’s good to know that frameworks and runtimes around us take advantage of this technique all the time.

Appendix: Seeing this in action

Let’s fire up a debugger with this amazing program:

class Program
static int Foo(int a, int b)
return a ^ b;

static void Main(string[] args)
Foo(5, 3);

I set a breakpoint at the call to Foo so that we can inspect the method descriptor for the method. Here it is:

0:000> !name2ee *!CheckingJITting.Program.Foo
Module: 69171000 (mscorlib.dll)
Module: 001d2354 (sortkey.nlp)
Module: 001d2010 (sorttbls.nlp)
Module: 001b2f2c (CheckingJITting.exe)
Token: 0x06000001
MethodDesc: 001b32c0
Name: CheckingJITting.Program.Foo(Int32, Int32)
Not JITTED yet. Use !bpmd -md 001b32c0 to break on run.

0:000> dd 001b32c0
001b32c0 40000001 20200005 001bc011 71030002
001b32d0 00200006 00250070 00060003 00000004
001b32e0 00000000 0000000c 00050011 00000004
001b32f0 693d84c0 001b2f2c 001b331c 001b1354
001b3300 00000000 00000000 69336a90 69336ab0
001b3310 69336b20 693a74c0 001bc019 00000080
001b3320 00000000 00000000 00000000 00000000
001b3330 00000000 00000000 00000000 00000000

If you look at method.hpp in the clr\src\vm folder of the SSCLI release, you’ll see that a method descriptor is in fact only the first two DWORDs here. The subsequent DWORD is the address of the jitted code, if it’s jitted (right now it isn’t—we haven’t called Foo yet).

Caveat: Using the SSCLI to examine the CLR data structures is risky because they are subject to change without any notice between service packs or .NET releases, and because they are not guaranteed to be in sync with any CLR version, for that matter.

Now let’s give the method a chance to run, and put a data breakpoint on the first DWORD of the method descriptor which contains flags (including the flag used to determine whether the method is jitted or not—as shown in the !DumpMD output above):

0:000> ba w 4 001b32c0

0:000> g
Breakpoint 1 hit
eax=00000030 ebx=00000000 ecx=001b32c0 edx=30000000 esi=0036ec60 edi=00000001
eip=6c741980 esp=0036ec08 ebp=0036ec0c iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
6c741980 c3 ret

0:000> k
ChildEBP RetAddr
0036ec0c 6c75b5dc mscorwks!OrMaskMP+0x3
0036ec1c 6c7f5da2 mscorwks!MethodDesc::SetCriticalTransparentInfo+0x23
0036ec50 6c758cc8 mscorwks!MethodSecurityDescriptor::ComputeCriticalTransparentInfo+0xb9
0036ec58 6c7f5d42 mscorwks!MethodSecurityDescriptor::IsCritical+0x10
0036ec74 6c7f5ba0 mscorwks!SecurityTransparent::IsMethodTransparent+0x39
0036ec84 6c7f8230 mscorwks!Security::CanSkipVerification+0x2f
0036ec9c 6c7f7889 mscorwks!GetCompileFlags+0xab
0036f034 6c7f771f mscorwks!UnsafeJitFunction+0x14a
0036f0d8 6c75120b mscorwks!MethodDesc::MakeJitWorker+0x1a8
0036f130 6c7513cb mscorwks!MethodDesc::DoPrestub+0x41b
0036f180 00b4087e mscorwks!PreStubWorker+0xf3
WARNING: Frame IP not in any known module. Following frames may be wrong.
0036f1c0 6c741b4c 0xb4087e
0036f1d0 6c752219 mscorwks!CallDescrWorker+0x33
0036f250 6c766591 mscorwks!CallDescrWorkerWithHandler+0xa3
0036f394 6c7665c4 mscorwks!MethodDesc::CallDescr+0x19c
0036f3b0 6c7665e2 mscorwks!MethodDesc::CallTargetWorker+0x1f
0036f3c8 6c82d7e5 mscorwks!MethodDescCallSite::CallWithValueTypes_RetArgSlot+0x1a
0036f52c 6c82d705 mscorwks!ClassLoader::RunMain+0x223
0036f794 6c82dc55 mscorwks!Assembly::ExecuteMainMethod+0xa6
0036fc64 6c82de3f mscorwks!SystemDomain::ExecuteMainMethod+0x456

It doesn’t really matter where exactly we are on the stack other than the fact that we’re inside code related to the JIT (see mscorwks!PreStubWorker on the call stack?). In other words, the method is in the process of being compiled, and after the call completes we have the following values for the method descriptor:

0:000> dd 001b32c0
001b32c0 71000001 20200005 002500a0 71030002
001b32d0 00200006 00250070 00060003 00000004
001b32e0 00000000 0000000c 00050011 00000004
001b32f0 693d84c0 001b2f2c 001b331c 001b1354
001b3300 00000000 00000000 69336a90 69336ab0
001b3310 69336b20 693a74c0 001bc019 00000080
001b3320 00000000 00000000 00000000 00000000
001b3330 00000000 00000000 00000000 00000000

The values of the flags have changed, and the subsequent address points to the actual method:

0:000> !DumpMD 001b32c0
Method Name: CheckingJITting.Program.Foo(Int32, Int32)
Class: 001b1354
MethodTable: 001b32e0
mdToken: 06000001
Module: 001b2f2c
IsJitted: yes
CodeAddr: 002500a0

0:000> !u 002500a0
Normal JIT generated code
CheckingJITting.Program.Foo(Int32, Int32)
Begin 002500a0, size 9
>>> 002500a0 55 push ebp
002500a1 8bec mov ebp,esp
002500a3 33ca xor ecx,edx
002500a5 8bc1 mov eax,ecx
002500a7 5d pop ebp
002500a8 c3 ret



Published at DZone with permission of Sasha Goldshtein, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.


Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.


{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}