Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Jan 25

Understanding Ahead-of-Time (AOT) compilation in C#

Understanding Ahead-of-Time (AOT) compilation in C#
Автор:
Источник:
Просмотров:
6459

A deep dive into Ahead-of-Time (AOT) compilation for optimized performance

What is it?

Traditionally, .NET languages like C# use Just-in-Time (JIT) compilation. In this process, the source code is initially compiled into Common Intermediate Language (CIL) code, a platform-agnostic code representation. Only when the code is executed does the JIT compiler translate the CIL code into machine code that the CPU processor can understand.

1. Source Code (C#)
     |
     | (Compilation to CIL)
     V
2. Common Intermediate Language (CIL) Code (published/deployed)
     |
     | (JIT Compilation at Runtime)
     V
3. Machine Code (Platform-Specific)
     |
     | (Execution)
     V

Ahead-of-Time (AOT) Compilation, on the other hand, introduces a paradigm shift in this process. Instead of waiting for execution time, AOT compilation converts the CIL code into machine code ahead of time, hence the name. This conversion can occur at build time, installation time, or any other time before execution. As a result, when the application is launched, the machine code is ready to be processed without the need for further compilation.

1. Source Code (C#)
     |
     | (Compilation to CIL)
     V
2. Common Intermediate Language (CIL) Code
     |
     | (AOT Compilation before Runtime)
     V
3. Machine Code (Platform-Specific) (published/deployed)
     |
     | (Execution)
     V

How it works

The AOT Compilation process in C# commences at build time, similar to JIT, transforming the high-level C# code into Common Intermediate Language (CIL). However, instead of waiting for the execution time to convert CIL into machine code, the AOT compiler takes over at this stage.

Firstly, the AOT compiler performs a static analysis of the entire codebase, not just the parts executed during runtime. It sifts through the application code, libraries, and all dependencies, converting them into native machine code.

Then, this machine code is linked with the runtime libraries essential for the application’s operation. The linking step generates an executable that contains the application’s code and all necessary runtime libraries, excluding the parts not used by the application, thereby reducing the final executable size.

Finally, when the application is launched, the already compiled machine code is directly executed, bypassing the need for any runtime compilations.

Why AOT?

Ahead-of-Time (AOT) compilation in C# brings numerous advantages, one of the most significant of which is the elimination of dependency on the .NET runtime during execution.

The Just-In-Time (JIT) compilation approach, which is a cornerstone of the .NET framework, facilitates the “write once, run anywhere” concept. But this .NET runtime requirement can sometimes prove to be a hurdle, particularly when deploying applications to environments where the installation of additional runtime systems is not desirable or feasible. For instance, certain embedded systems, minimal Docker containers, or other resource-constrained environments may not support or allow the inclusion of a full .NET runtime.

AOT significantly simplifies the process of publishing .NET applications across different platforms. Developers can compile their applications into the specific form required for each platform. As a result, deploying these applications is more straightforward because it’s no longer necessary for the target system to have the .NET runtime installed.

For example, if we want to deploy a .NET application on a Linux system, we can use AOT compilation to compile the application into a form that’s directly executable on Linux. This way, we can distribute the application without worrying about whether the target system has the .NET runtime.

The AOT compilation’s decoupling from the .NET runtime brings several other benefits:

  • Faster Start-up Time: Since the code is pre-compiled to the native machine code, the application can start and run quickly, skipping the step of JIT compilation at runtime.
  • Improved Execution Performance: With AOT, the entire application code is available for optimization at compile time, potentially leading to more efficient execution.
  • Increased Security: The conversion to native code ahead of time can obscure code logic, making it more challenging for malicious actors to reverse-engineer the application.
  • Reduced Memory Footprint: Without the need for the .NET runtime and JIT compiler in memory, the application’s memory footprint can be smaller, especially beneficial for devices with limited memory resources.

Native Ahead-of-Time (AOT) compilation offers significant benefits for workloads with a high number of deployed instances, like cloud infrastructures and hyper-scale services.

In such scenarios, the AOT-compiled applications are transformed into native code ahead of time, which allows them to start faster and perform better since they don’t have to spend time on Just-in-Time (JIT) compilation during runtime. This can lead to substantial performance improvements and cost savings, especially in large-scale cloud environments where scaling rapidly and efficiently managing resources are key.

Additionally, AOT-compiled binaries are standalone and have fewer dependencies, simplifying the deployment process and making the application more robust against variations in the deployment environment.

How to use

Before you can publish .NET projects with native Ahead-of-Time (AOT) compilation, you must first meet a few prerequisites. If you’re using a Windows system, you’ll need to install Visual Studio 2022. In addition to the standard installation, ensure you’ve included the ‘Desktop development with C++’ workload and all of its default components.

Desktop development with C++

Let’s dive into a practical illustration. We’re going to create a simple .NET 7.0 console application.

.NET 7.0 console application

This will generate a basic “Hello World” example.

namespace AOTExample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

We can compile this project and publish to a local folder. This will generate a Framework-dependent deployment.

Profile settings

The published files looks like following:

Published files

Now we modify the project file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Adding one line:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
  </PropertyGroup>

</Project>

Then run the following command:

dotnet publish -r win-x64 -c Release

dotnet publish

It publishes the app for Windows as a native AOT application:

Native AOT application

The noticeable increase in the EXE file size is due to the Ahead-of-Time (AOT) compilation, which eliminates the dependency on the .NET runtime.

Is that all? Does this mean we can compile any type of application for native AOT deployment?

Limitation

We modified our program as follows: it essentially uses HttpClient to call an API and display the response.

namespace AOTExample
{
    internal class Program
    {
        static async Task Main()
        {
            try
            {
                HttpClient client = new HttpClient();
                HttpResponseMessage response = await client.GetAsync("https://httpbin.org/anything");
                response.EnsureSuccessStatusCode();
                string responseBody = await response.Content.ReadAsStringAsync();

                Console.WriteLine(responseBody);
            }
            catch (HttpRequestException e)
            {
                Console.WriteLine("\nException Caught!");
                Console.WriteLine("Message :{0} ", e.Message);
            }
        }
    }
}

We compiled it for native AOT deployment, and it worked.

Native AOT deployment

Now, we’re adding a third-party package, Newtonsoft.Json, and modifying the program as follows:

using Newtonsoft.Json;

namespace AOTExample
{
    internal class Program
    {
        public class Customer
        {
            public int Id { get; set; }
            public string? Code { get; set; }
            public string? Name { get; set; }
        }

        static async Task Main()
        {
            // Define the customer
            var customer = new Customer
            {
                Id = 1,
                Code = "CUST01",
                Name = "John Doe"
            };

            // Convert the customer to a JSON string
            var json = JsonConvert.SerializeObject(customer);

            try
            {
                HttpClient client = new HttpClient();
                var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
                HttpResponseMessage response = await client.PostAsync("https://httpbin.org/anything", content);
                response.EnsureSuccessStatusCode();
                string responseBody = await response.Content.ReadAsStringAsync();

                Console.WriteLine(responseBody);
            }
            catch (HttpRequestException e)
            {
                Console.WriteLine("\nException Caught!");
                Console.WriteLine("Message :{0} ", e.Message);
            }
        }
    }
}

We ran the compile command, which generated numerous warnings. However, it did succeed in compiling the program.

Compiling the program

Now, let’s run the native deployment app:

Run the native deployment app

It’s obviously not working properly. If we run the .NET runtime version, we can see that the response includes the JSON customer payload.

JSON customer payload

It seems that the following code doesn’t work in the native AOT app. Based on the API response, it ends up as an empty string.

// Convert the customer to a JSON string
var json = JsonConvert.SerializeObject(customer);

Could this issue be due to the third-party library, NewtonSoft.Json? Let’s revise the code, removing the NewtonSoft.Json package and instead employing the built-in JSON serializer provided by .NET.

using System.Text.Json;

namespace AOTExample
{
    internal class Program
    {
        public class Customer
        {
            public int Id { get; set; }
            public string? Code { get; set; }
            public string? Name { get; set; }
        }
        static async Task Main()
        {
            // Define the customer
            var customer = new Customer
            {
                Id = 1,
                Code = "CUST01",
                Name = "John Doe"
            };

            // Convert the customer to a JSON string
            var json = System.Text.Json.JsonSerializer.Serialize(customer);

            try
            {
                HttpClient client = new HttpClient();
                var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
                HttpResponseMessage response = await client.PostAsync("https://httpbin.org/anything", content);
                response.EnsureSuccessStatusCode();
                string responseBody = await response.Content.ReadAsStringAsync();

                Console.WriteLine(responseBody);
            }
            catch (HttpRequestException e)
            {
                Console.WriteLine("\nException Caught!");
                Console.WriteLine("Message :{0} ", e.Message);
            }
        }
    }
}

The first thing we notice is that Visual Studio immediately provides a warning:

Visual Studio warning

Proceeding with the AOT compilation, we find that it compiles successfully. However, the application’s outcome remains unchanged — the following code segment still does not operate as expected.

var json = System.Text.Json.JsonSerializer.Serialize(customer);

The AOT compilation process incorporates a step known as ‘trimming’, which essentially eliminates unused code before the compilation into native code takes place. The purpose of trimming is to keep the final compiled file size minimal. As you might imagine, without trimming, the file would include all the .NET libraries and any third-party libraries used by the application, resulting in an extraordinarily large final file.

Given the application’s complexity, only a subset of framework assemblies might be referenced, and out of these, only a fraction of the code within each assembly is necessary to run the application. The parts of the libraries that go unused are superfluous and can be removed from the packaged application.

On the surface, trimming seems straightforward: during the application’s publishing process, the .NET SDK scrutinizes the entire application, removing all unused code. However, identifying what constitutes as ‘unused’, or more accurately, ‘used’, can pose a challenge. For instance, the library, System.Text.Json, relies extensively on Reflection. This allows it to discover or emit types from user code at runtime, complicating the analysis process and making it harder to determine what code is in use.

For the particular issue highlighted in our example, there is a viable solution. We can employ source generation in System.Text.Json. This method ensures the compiler is cognizant of the types that will be utilized, thus circumventing the problem.

However, trimming/reflection is not the sole limitation of AOT compilation. There are additional constraints to consider, although some of them may be improved upon in the future.

Native AOT applications come with a few fundamental limitations and compatibility issues. Here are the main ones:

  • Absence of dynamic loading, such as Assembly.LoadFile.
  • Absence of runtime code generation, for instance, System.Reflection.Emit.
  • Inability to support C++/CLI.
  • Absence of built-in COM (only relevant to Windows).
  • Requirement for trimming, which possesses its own limitations.
  • Implicit compilation into a single file, which is known to have certain incompatibilities.
  • Applications incorporate necessary runtime libraries, which enlarges their size compared to framework-dependent apps (similar to self-contained apps).
  • System.Linq.Expressions always employs their interpreted form, which is slower than runtime-generated compiled code.

At present, .NET 7 supports AOT compilation only for console apps. However, the future is promising as ASP.NET Core 8.0 will introduce native ahead-of-time (AOT) support in the upcoming .NET 8 release.

Something in Between

AOT compilation is still under active development. Until it matures, we may encounter roadblocks for complex real-world applications. However, there exist several deployment models between the framework-dependent and native AOT deployments, which can resolve certain specific issues for various scenarios.

  • The self-contained deployment model in .NET represents an evolution in flexibility and reliability for your applications. The output publishing folder contains all components of the app, including the .NET libraries and target runtime. The app is isolated from other .NET apps and doesn’t use a locally installed shared runtime. The user of your app isn’t required to download and install .NET.

    <PropertyGroup>
        <SelfContained>true</SelfContained>
    </PropertyGroup>
  • The trim-self-contained deployment model refines the self-contained deployment model, focusing on minimizing the deployment size. In essence, the trimming process evaluates the complexity of the application, identifying which framework assemblies are referenced and what portions of the code within each assembly are required for the application to run. The rest, being unused and hence unnecessary, is ‘trimmed’ from the application package. By discarding these unused parts of libraries, the trim-self-contained deployment model achieves its primary goal: to reduce the size of the deployment package. This results in a more lightweight, efficient application that delivers the same functionality while consuming less resources.

    <PropertyGroup>
        <PublishTrimmed>true</PublishTrimmed>
    </PropertyGroup>
  • Single-file deployment is a deployment mode that packs your application and all its dependencies, including the runtime, into a single executable file. Single-file deployment is available for both the framework-dependent deployment model and self-contained applications.

    <PropertyGroup>
        <PublishSingleFile>true</PublishSingleFile>
    </PropertyGroup>
  • ReadyToRun (R2R) compilation is a powerful tool for improving the startup performance of .NET applications. By adopting a form of Ahead-of-Time (AOT) compilation, R2R reduces the workload for the Just-In-Time (JIT) compiler at the time your application launches. When creating R2R binaries, both the native code (akin to what the JIT compiler would produce) and the Intermediate Language (IL) code are included. The IL code remains essential for certain scenarios, but by having the native code readily available, the JIT compilation process is considerably expedited, thereby improving startup performance.

    <PropertyGroup>
        <PublishReadyToRun>true</PublishReadyToRun>
    </PropertyGroup>

Final thought

The rapid evolution and adoption of Ahead-of-Time (AOT) compilation in compelling scenarios like ASP.NET Core and Blazor’s WebAssembly host clearly demonstrate its transformative potential. The performance enhancements and deployment advantages it provides continue to position AOT as a technology of promise. This innovative technique is poised to usher the .NET ecosystem into a new era of excellence and efficiency.

Похожее
Aug 8
Author: Anton Martyniuk
Integration testing is a type of software testing essential for validating the interactions between different components of an application, ensuring they work together as expected. The main goal of integration testing is to identify any issues that may arise when...
Aug 27
Author: Anton Martyniuk
Multitenancy is a software architecture that allows a single instance of a software application to serve multiple customers, called tenants. Each tenant's data is isolated and remains invisible to other tenants for security reasons. This architecture is commonly used in...
May 13, 2023
Author: Juan Alberto España Garcia
Introduction to Async and Await in C# Asynchronous programming has come a long way in C#. Prior to the introduction of async and await, developers had to rely on callbacks, events and other techniques like the BeginXXX/EndXXX pattern or BackgroundWorker....
Sep 1
If you’re a developer faced with the decision of selecting between Windows Presentation Foundation (WPF) and Windows Forms (WinForms) commonly referred to as WPF vs WinForms, you may be eager to understand the distinctions between these two UI frameworks. In...
Написать сообщение
Тип
Почта
Имя
*Сообщение
RSS
Если вам понравился этот сайт и вы хотите меня поддержать, вы можете
Soft skills: 18 самых важных навыков, которыми должен владеть каждый работник
Мультитаскинг, или Как работать над несколькими проектами и не сойти с ума
Using a сustom PagedList class for Generic Pagination in .NET Core
Как мы столкнулись с версионированием и осознали, что вариант «просто проставить цифры» не работает
9 главных трендов в разработке фронтенда в 2024 году
Рассуждение на тему, какую базу данных выбирать
Бредовая работа
Доводим разработчика до выгорания: три простых шага
Вопросы с собеседований, которые означают не то, что вы думаете
Семь итераций наивности или как я полтора года свою дебютную игру писал
LinkedIn: Sergey Drozdov
Boosty
Donate to support the project
GitHub account
GitHub profile