Introduction
This article will primarily focus on understanding how JIT compilers work to ensure that our written code converts to machine code and how they are evaluated to improve optimization performance, especially with Dynamic PGO. We will also discuss what ahead-of-time (AOT) is and how it can be enabled in our application using Visual Studio.
Understanding JIT Compiler
With their own compilers and runtimes, the majority of programming languages may optimize or customize according to their needs and rely less on the operating system. This is also true of .NET, and for a long time we have been hearing about a .NET compiler that translates code into an intermediate language. This is the initial stage of compilation; in order for the underlying operating system to comprehend the code in this intermediate language and begin running our application, we must carry out an additional level of compilation during runtime.
In the realm of.NET, the Just-In-Time (JIT) compiler is more widely used, and we have been learning about it for a while. However, since the debut of.NET, the majority of developers have not understood how this compiler functions or how it is evaluated. To put it simply, when we write our logic, which may contain multiple methods, and then try to run the application, the JIT compiler will recognize each method when it is called, translate it into a set of instructions (machine code), and store it so that when the same method is called, it will reuse the existing machine code rather than regenerating the same instructions, improving the application execution time.
Now that the JIT compiler is able to avoid spending more time creating the machine code for the next method calls, everything appears to be in order. However, JIT will continue to spend more time optimizing the code for subsequent calls that are useless if we have a collection of methods that only call once. Since the introduction of.NET Core 3.0 Tired Compilation, this issue has been resolved.
JIT Tired Compilation
JIT will now maintain two tiers (tier 0 and tier 1). The JIT compiler will compile the method to machine code with minimal optimization logic when it is called for the first time, keeping it in Tier-0. If the method calls a predetermined threshold limit (let's say 5), the compiler will perform full optimization of the method and keep it in Tier 1. Our benefit is that, in comparison to conducting full optimization, the compiler won't take longer to complete minimal optimization. In this manner, when compared to conventional.NET Frameworks, we can observe greater performance gains in our.NET core apps.
Understanding Profile-guided optimization (PGO)
PGO, which has been used in other programming languages to improve runtime speed, has been around since .NET 6. In .NET 6 and .NET 7, PGO is by default disabled because the developers took more time to enhance this technology for our runtime. PGO is now enabled by default in .NET 8, and as a result, we are witnessing significantly greater performance gains than in earlier iterations.
PGO essentially allows the application's instrumentation and learns how the function is running. It then optimizes the code appropriately, greatly enhancing execution performance. When the method is in Tier-0, this instrumentation information will be linked to it; when it moves to Tier-1, it will use them.
Ahead Of Time (AOT) Compiler
Another kind of compiler is called Ahead of Time (AOT), which compiles and produces native code to an executable file or self-contained library at build time. The advantage we have is that it runs independently and quickly at startup, and it won't rely on the framework.
Since .NET 7, native AOT support has been available, and it is being improved for upcoming versions in terms of build time and self-contained library size. Using the Visual Studio 2022 IDE, we will now create a basic console application to demonstrate how to activate this AOT capability.
When choosing the Framework for a new project, we may see a checkbox that lets us activate this feature. This is unchecked by default.
Once you have created the project, you can observe a couple of changes here.
- Additional property PublishAot to the project file got added as shown below.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
</Project>
- A couple of analyzers were added which keep monitoring the build process to notify in case of any compatibility issues.
After they are configured, we can publish the program and observe the creation of an executable file.
Note. To see this AOT publish option, we need to make sure to install Desktop development using a C++ workload.
Summary
This article helps you to better understand how the JIT compiler works using tiered compilation and PGO methodology and learn how to enable AOT in our application using Visual Studio.