Have you ever wondered how a C# code executes? What is going on in the background? In this blog, let's take a deep dive into the background of C# code execution and the key component called CLR, which is in charge of executing our code.
Before we dive into the core CLR, let's understand a few basic concepts,
What is a managed code?
Code that we develop using a language compiler that targets a runtime is called managed code, or we can simply say its code, which runs under the control of a Runtime environment.
Why Managed Code?
Managed code with a runtime environment gives a number of benefits.
- Memory Management
- Security
- Cross Platform compatibility
- Cross-language integrity
- Exception handling
- Debugging and profiling services etc.
How does It work?
Language compilers produce metadata and a common Intermediate language (CIL) that describes types, members, and references and will be part of every runtime loadable portable executable (PE) file such as dll, exe. The runtime uses this method to locate and load classes, layout memory, and resolve method invocation, and during the execution time, the compiler available within the run time will translate CIL to native code. The managed code execution process can be summarized as.
- Compiling source code to CIL by Language compiler]
- Translating the CIL to native code during execution
- Running the code using the metadata from the PE file with provided memory management, Type checking, and exception handling.
Great Now we know what managed code is and how it works with a runtime. Let’s Jump into Core CLR.
Core CLR is a runtime environment provided by .net core, responsible for executing managed codes. There are various language compilers like Visual Basic, C#, Visual C++, F#, Perl, and COBOL are available which target Core CLR runtime. Let’s dig deeper into the CLR by taking C# as an example.
When compiling a C# code, our language compiler will generate metadata and also convert the code to Common Intermediate Language (CIL), formerly known as MSIL( Microsoft Intermediate Language) which is a low-level, CPU-independent set of instructions. It is an intermediate that is generated by a language compiler which will be further compiled to machine code by CLR. This is fed to CLR.
Now Let’s see what the role of CLR is.
CLR has to execute the instructions provided in the form of CIL, and to do that CLR has to compile CIL to native code based on target machine architecture. .NET provides two ways to achieve this.
- Just-in-time compiler (JIT)
- Native Image Generator
Just-in-time compiler
JIT converts the CIL to machine code during runtime on demand when the content of assembly is loaded and executed.
How it works?
- The method is executed, CLR hands over the CIL for the method to JIT
- JIT will translate the code to machine code
- The Generated machine code is cached, so the subsequent calls to the method will be handled from the cache.
- Native machine code is executed by the CPU.
Types of JIT
.net core runtime includes several types of JIT compilation strategies.
Tired Compilation
Aims to provide fast startup and High throughput. It involves compiling the methods in two tiers.
- Tier 0 – Quick JIT: Methods are initially compiled with minimal optimization to provide faster throughput
- Tier 1 – Optimized JIT: Methods that are executed frequently are recompiled with higher optimization.
Regular JIT
These are the regular JIT, Where the methods are initially compiled with higher optimization and cached.
- Pre-compilation (Ready to Run – R2R): R2R is a case of ahead-of-time compilation, where the CIL code is compiled to native code during the build time itself.
- Dynamic Profile Guided Optimization: This is a highly advanced option, where the runtime collects profiling information about the application while it runs and uses this data to optimize the code.
Native Image Generator
JIT compiler converts the CIL code to machine code when methods defined in the assembly are invoked. This will have a negative impact on the performance, and also, the code generated by the JIT compiler is bound to the process that triggered the compilation. It cannot be shared among other processes. To allow the generated machine code to be shared among multiple processes, the CLR supports ahead-of-time compilation (AOT) mode. This mode uses Ngen.exe I to convert CIL to machine code as JIT does, but slightly in a unique way.
How does Ngen.exe work?
It performs CIL to Machine code conversion before running the application. It compiles the entire assembly, one at a time, and stores the machine-generated code in Native Image Cache as a file on the disk.
Now let’s go back to the CLR, Let’s see what the other functionalities of CLR other are than converting CIL to Machine Code.
Another key role of the CLR is to perform Verification of the CIL code. The CLR always examines the CIL code to make sure it is type-safe. Type safety means the code only accesses the memory locations it is authorized to do. This type of safety verification makes sure the objects are isolated from each other and it avoids any kind of corruption. CLR uses various verification steps like,
- Verifies the value assigned to the variable matches the declared types.
- Verifies the method calls using the correct number and type of the arguments.
- Validate the meta data and make sure the members are defined correctly matching their metadata, external libraries used are resolved properly.
Like these, various validations are available. Finally, the CLR does the job of executing the generated machine code. During this time CLR provides a lot of added functionalities like Garbage Collection, Thread Handling, Interoperability, etc. These we will discuss in detail in my upcoming blog.
Let’s Summarize what we read.