Knowing the idea and main benefits of JIT compilation from the previous post, we’ll now see how it fits into .NET applications execution model.
By execution model I mean a process of having a .NET Framework application actually executed on the machine (CPU), starting from having its source code written. It contains all steps and actions necessary to happen in order to transform source code (like C#) into machine (assembly) code and execute it.
.NET languages standardization
As we all know, .NET is just an execution platform. There are, however, a lot of programming languages which allow us to write code which can then be executed by the platform-specific (not language-specific!) CLR. In order to give programming languages’ creators a set of rules which their language and its compiler must meet to be .NET-compliant, Microsoft defined a Common Language Infrastructure (CLI, also referred to as MSIL in the old days 😉 ) which is also standardized by ISO and ECMA.
The main goal of it is to define how the source code should be compiled to the Common Intermediate Language (CIL). As we already know, the CIL should have a standardized format, because it’s then JIT-compiled to a platform-specific assembly code. Some of the most common implementations of CLI are .NET Framework, .NET Core and Mono. I recommend you to check Matt Warren’s article on .NET Runtimes history as well 🙂
Common Language Infrastructure implies also some other aspects of the programming language and its compiler, including Common Type System, language-agnostic metadata and Common Language Specification. You can read more about it for instance here.
This all allows us to use various programming languages, like C#, VB.NET, F# or JScript .NET to develop .NET applications. Anyone can still implement their own own .NET language – it “just” needs to conform to the standards.
There’s also another term introduced somewhat around .NET Core – .NET Standard. Its purpose is to allow code sharing between various .NET implementations. It actually defines a set of APIs that each .NET platform should implement. More details here.
This whole standardization makes us arriving to the .NET applications execution model, which is how every standards-compliant CLR application is executed.
.NET execution model
Independently of the language of choice, .NET application’s execution model can be described as the following 4-steps process:
Writing source code in a programming language of choice, including the usage of its compiler
Compiling source code to Common Intermediate Language (CIL)
These steps are well-resented on the schema below and then described in details in the next sections.
Writing source code in a programming language of choice
As mentioned before, .NET applications can be implemented using any language of choice, which is compliant with CLI. Another important part is a compiler – it defines a general syntax of the language, kinds of data types that can be used by a programmer etc. For instance, current open-source compiler for C# and Visual Basic is Roslyn.
In the end, role of the compiler is to transform the source code to its CIL equivalent.
The simplest possible complete application written in C# can look as follows:
Compiling source code to CIL
After the source code is written, it must be compiled to CIL, which is an intermediate language understood by the CLR. On this level, the whole execution process becomes language-agnostic, which means that as soon as the code is compiled to CIL it is executed by the CLR and programming language’s features (e.g. its compiler) don’t play any more role.
CIL code looks more like assembly code, however containing some specific instructions. It’s still far more readable than native code. Apart from the direct transcript of source code, CIL contains metadata about the DLL/EXE assembly file.
The C# fragment presented in the previous section compiled to CIL by Roslyn looks as follows:
A bit more than the original source code, isn’t it? 😉
Firstly there is a metadata instructions section which contains information about the assembly, execution framework’s version, types used in the code and external references. Next there’s the actual CIL code (equivalent to C# code presented above) listed.
By default, JIT compilation is done in the place where “Normal JIT Compiler” is drawn on the schema below:
IL instructions of a particular block of code, in the example diagram of a method Foo(), are JIT-compiled when the block is to be used for the first time. Then, this JIT-compiled code is stored as the JIT-ed equivalent of Foo() method’s code in the memory cache. Such native code is then executed and if the same block of code needs to be called again in the future, it will not be re-JIT-compiled, but instead the native code will be taken from the memory cache and executed again.
There’s a possibility to skip adding the native code into memory cache for future executions and it’s referred to as “Econo JIT Compilation”, however it’s not widely used and probably obsolete since few versions of .NET, so we won’t examine it here.
The schema presented above changes a bit as soon as we want to use so called Pre-JIT compilation. We mentioned it already and we know that one of the techniques to allow pre-JIT (or ahead-of-time) compilation is using Native Image Generator (ngen.exe) which allows to transform CIL assemblies into native code files and to store them as a file on the disk (in the Native Image Cache). It allows to natively compile the whole assemblies – it doesn’t allow to compile only single methods as JIT compiler does.
Refer to the new version of the schema presented below to see how the process changes with NGen.
Normal and ahead-of-time CIL compilation modes provide different cons and pros, including:
NGen delivers faster start-up time, especially in large applications used by many users in the same time, but requires more disk space and memory to store both the CIL and pre-compiled images,
JIT compilation at runtime can deliver faster code, because it targets the current platform of execution; NGen produces the native images with instructions that can be executed on all possible platforms, meaning it must use the oldest from currently used instructions sets to be backwards-compatible,
JIT is able to dynamically re-compile the code for a better performance depending on the execution conditions (e.g. by detecting a hot path in the executed code).
Choosing one of the modes depends on your use case.
Execution of the native code
Finally, the code blocks are run by the CLR, which asks CPU to execute compiled instructions sets. If you’re interested in how CPU exactly works and executes the instructions, I recommend you watching this YT video.
We’ve seen today how the .NET applications are executed step-by-step, starting from source code (like C#) up to being actually executed by the processor.
I also encourage you to read – if you haven’t already – all the previous posts in the .NET Internals series. Just click here to see them all 🙂
Let me know if you have any doubts or thoughts, I’m always open to your comments and opinions 😉 Especially the constructive ones pointing out some parts I omitted or didn’t describe precisely enough.
I hope it helped you learn something new 🙂