I spent last few weeks working on startup time improvements for a .NET Windows application. Over the next few weeks I’m going to share this experience and write a series of posts about JIT compilation and some interesting stuff you can do with it.
Let’s start. Today I am going to talk about JIT Compilation: what it is and how it works.
There are two types of compilers: compilers which do explicit compilation and compilers which do implicit one.
Explicit Compilation is a process when compiler converts source code directly to machine code which is understandable by CPU. C and C++ compilers are great examples of Explicit Compilation.
Implicit Compilation is a two-steps process, and it requires a Virtual Machine to be able to execute your code. The first step of the process is converting your program to a bytecode understandable by Virtual Machine. .NET bytecode is called Common Intermediate Language or CIL. It is also known as Microsoft Intermediate Language (MSIL) or just Intermediate Language (IL). I am going to use its official name Common Intermediate Language (CIL). The first step is done by a compiler (C#, VB.NET, etc.).
The second step is converting CIL code to a machine code running on metal. This is Virtual Machine’s task. Common Language Runtime (.NET Virtual Machine) converts only executed CIL fragments into CPU instructions at runtime.
Just-In-Time compilation the process of converting CIL to machine code translation. In .NET world it is done by JIT Compiler (JIT or Jitter) which is a part of Common Language Runtime.
Let’s take a look how it works. You run a .NET application. First of all, CLR creates an internal data structure for all reference types and their methods. Each method references a procedure compiling CIL to machine instructions. Next your program calls a method. CLR sees that the methods points to the Jitter and starts it. Compiler reads CIL from metadata, verifies it, allocates memory and compiles the bytecode. Then the reference to JIT compiler is replaced with a reference to the compiled block of code. The last step is to jump to the machine code and run it.
Next time, when program calls the same method, CLR executed compiled CPU instructions.
This process adds a little overhead for the first method call. Usually your application calls methods over and over again, and you will not see any performance problem at all.
JIT stored compiled code in dynamic memory. It means that you will compile your application twice if you run it two times simultaneously.
Jitter does not persist its work between runs of you application. The reason for that is it checks your current system state (CPU type, CPU count, etc.) and tries to generate optimized code for your current situation. When you will run you application next time, you may have different system state, and Jitter’s output will be different as well.
There are two types of JIT compilers available in the latest .NET Framework version.
Normal-JIT Compiler. This is the compiler I have described above. It compiles methods in runtime as needed.
Pre-JIT Compiler. It compiles the entire assembly before it has been used. Usually the compilation happens during application deployment. You can pre-compile an assembly using Ngen.exe tool (Native Image Generator).
.NET Framework 1.0 and 1.1 had the third JIT Compiler - Econo-JIT. It was a runtime compiler which hadn’t done any optimization and removed methods’ machine code when it is not required. It was done to improve compilation time. The compiler is obsolete since .NET 2.0 and you cannot enable it anymore.
JIT vs. Ngen
At the end I would like to compare Normal-JIT compiler and Pre-JIT compiler (Ngen).
Normal-JIT compiler is the best option for almost all cases because
It may take advantage of your current hardware. For example, it will use CPU specific instructions to achieve better performance.
It will analyze your execution flow and remove unused loops and branches.
Jitter definitely works well for server-side applications when the application just works, and works, and works.
The only case when you may start thinking about Ngen are Windows Clients. Let’s say, you have huge Windows Client Application It may take long time to startup, because JIT compilation will take too much time and CPU. It is not a problem for server-side applications, but might be a problem for actual users’ software: people do not like waiting for 5 minutes to be able to run an app.
My next article in the series will be all about Ngen. To be continued...