Within the realm of software development, optimizing performance and streamlining efficiency remain essential. The .NET platform has been innovating for 2 decades to provide developers with the infrastructure to craft resilient and efficient software solutions.
A notable stride merges with Native AOT (Ahead-of-Time) Compilation. The present article delves into .NET Native AOT, uncovering how it works, its benefits, and the various scenarios where it finds application.
What is .NET Native AOT?
.NET Native Ahead-of-Time (AOT) compilation stands as a cutting-edge advancement within the .NET platform. With AOT the C# code is compiled to native code on the developer machine. This contrasts with the traditional method where code is compiled to native code during runtime.
This is illustrated by the schema below. The .NET traditional compilation involves two steps:
- The C# compilation produces DLL(s) files that contain Intermediate Language (IL) code. Such DLL is named a .NET assembly.
- When executing a .NET program, the .NET runtime (the CLR Common Language Runtime) loads the .NET assemblies. A subsystem of the CLR is responsible for compiling IL code to native code directly executed by the CPU. This subsystem is named the JIT (Just-In-Time) compiler. It earns its name because it compiles the IL code of a method only when that method is first invoked.
On the other hand, .NET Native AOT compilation consists of a single step. Compiling the C# source code into native code on the developer’s machine. This process involves transforming the C# code into IL code and subsequently into Native code, forming a two-step compilation process. But this is an implementation detail. This is why the AOT .NET Assembly box is gray in the schema below.
Benefits of .NET Native AOT
The .NET Native Ahead-of-Time (AOT) compilation brings forth a spectrum of advantages:
- Enhanced Performance: By pre-compiling code to native machine instructions, .NET Native AOT significantly reduces startup times and improves overall application performance. In a serverless scenario where the app gets started for each request, this can make a significant difference. Also, the absence of JIT compilation overhead during runtime translates to faster execution, offering a smoother user experience.
- Simplified Deployment: AOT-compiled applications often result in standalone executables with zero or fewer dependencies. This simplifies deployment processes, making it easier to distribute applications across various platforms and devices without the need for additional installations or runtime components.
- Smaller Application Size: By trimming away unnecessary code, AOT can drastically reduce the size of the application. This not only saves storage space but also optimizes the application’s memory footprint, especially crucial in resource-constrained environments such as mobile devices or IoT devices.
- Enhanced Intellectual Property Protection: AOT compilation converts source code into optimized machine code, making it significantly more challenging for reverse engineering attempts. The resulting native code is more obfuscated and challenging to decipher than IL code that can be easily decompiled to the original C# code. This enhances the security of sensitive algorithms, business logic, and proprietary methods embedded within the application.
Disadvantages of .NET Native AOT
The benefits gained with AOT inevitably come with certain drawbacks. Here they are:
- Platform-Specific Compilation: .NET Native AOT produces platform-specific native code, tailored to a particular architecture or operating system. For example, unlike regular .NET assemblies, the executable produced on Windows with AOT won’t work on Linux.
- No support for cross-OS compilation. For example, from a Windows box, you cannot compile a Linux native version and vice-versa.
- Partial support for Reflection: Reflection relies on dynamic code generation and runtime type discovery, which conflicts with the precompiled and static nature of AOT-compiled code. However we’ll see at the end of this article that usual Reflection usages work pretty well with AOT.
- Require AOT Compatible Dependencies: AOT compilation requires that all libraries and dependencies used in the project are AOT-compatible. Libraries relying on reflection, runtime code generation or other dynamic behaviors might not be compatible with AOT, potentially causing conflicts or runtime errors.
- Increased Build Times: AOT compilation involves the upfront generation of native code during the build process. This additional step can significantly increase build times, especially for larger projects or applications with extensive codebases.
- Require Desktop Development Tools for C++: AOT can only compile with these tools installed which can weigh up to 7GB on your hard drive.
.NET Native AOT in Action
Creating a .NET Native AOT Web Application with Visual Studio
Start Visual Studio 2022 v17.8 (or upper) and choose to create a project from the template ASP.NET Core Web API (native AOT).
The template generates for us a .NET 8 ASP.NET Core Web API application. We can see that the .csproj file contains <PublishAot>true</PublishAot>
.
Compiling the .NET Web Application
If we compile the application we can see that a DLL file is produced under .\bin\debug\net8.0
. This DLL is a regular .NET assembly that contains IL code. We can decompile it with ILSpy for example as shown in the screenshot below:
Executing the .NET Web Application
At this point, no AOT compilation occurred. The C# code has been compiled into IL code in less than a second. The .NET web application can be executed as-is:
Compiling the Web Application with .NET Native AOT
To compile the Web Application with .NET Native AOT, you must type dotnet publish
in the Package Manager Console:
This time the compilation took 11 seconds for this small application. We obtain an executable file that weighs 9MB. This file cannot be decompiled with ILSpy because it is not a .NET assembly. This file contains only native code. A 70MB PDB file is also produced to link native code with the source code for debugging purposes.
The executable file can be deployed and executed as-is on any Windows x64 system. Could the deployment be easier? I don’t think so!
.NET Native AOT Trade-Off
It’s time to assess the options:
- On one hand, we get a 40KB DLL that works on all platforms generated in less than a second. But it requires that .NET 8.0 gets pre-installed.
- On the other hand, we get a 9MB executable compiled in 11 seconds. It works on any Windows x64 system with no prerequisite. Also, this native version’s start-up is faster. And we can expect more performance gain for any sufficiently complex Web Application.
One could think that 9MB is way larger than 40KB! But installing .NET 8.0 takes much more disk space than 9MB. Just the directory C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0
weighs 70MB and there are more things to get installed. Thus AOT leads to:
- Compact container images are particularly beneficial in containerized deployment setups
- Decreased deployment duration owing to smaller image sizes
Here is a comment on the present article that is worth quoting at this point: “How is this a cutting-edge advancement? It’s the way software has been produced for half a century prior to the introduction on the .NET platform.”
Here is my answer: Because you get the both of both worlds.
- On one hand, modern language and platforms bring many facilities through a runtime compared to C/C++
- and on the other hand, raw native executable without the need to install the platform on the production machine.
Pause a second and realize how cool is this single executable. Not only do these 9MB contain the web application code. But it contains some CLR code (garbage collection, library loading…), the .NET types commonly used (string, int…), and the APIs code (like WebApplication.CreateSlimBuilder() and the JSON serializer code).
The dependency graph below (obtained with NDepend) shows this web application code and its dependencies, all packed in the 9MB bits! Click it to obtain a larger image.
Breaking .NET Native AOT with Reflection?
I tried hard to break .NET Native AOT with Reflection but failed. This is good news that AOT supports a lot of the Reflection API. Here is the code that works with AOT:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// Reflection usage A System.Type type = new object().GetType(); System.Reflection.MethodInfo methodInfo = type.GetMethod("ToString"); string str = (string)methodInfo.Invoke("Walk the dog", BindingFlags.Default, null, new object?[0], null); Console.WriteLine("Reflection usage A:" + str); // Reflection usage B var listOfString = typeof(List<>).MakeGenericType(new Type[] { typeof(string) }); var list = Activator.CreateInstance(listOfString) as List<string>; list.Add("hello"); PropertyInfo prop = listOfString.GetProperty("Count"); int count = (int)prop.GetMethod.Invoke(list, new object?[0]); Console.WriteLine("Reflection usage B:" + count); // Reflection usage C Assembly assembly = Assembly.GetExecutingAssembly(); Type[] types = assembly.GetTypes(); foreach (Type type1 in types.Take(5)) { Console.WriteLine("Reflection usage C:" + type1.FullName); } // Reflection usage D Type unknownType = Type.GetType("System.String"); ParameterExpression param = Expression.Parameter(unknownType, "x"); MethodInfo method = unknownType.GetMethod("ToLower", Type.EmptyTypes); Expression call = Expression.Call(param, method); var lambda = Expression.Lambda(call, param).Compile(); var result = lambda.DynamicInvoke("HELLO"); Console.WriteLine("Reflection usage D:" + result); |
We get some warnings at dotnet publish
time but it works!
.NET 8 Support for Native AOT
Those are supported:
- Middleware
- Minimal APIs
- gRPC
- Kestrel HTTP Server
- Authorization
- JWT Authentication
- CORS
- HealthChecks
- OutputCaching
- RequestDecompression
- ResponseCaching
- ResponseCompression
- StaticFiles
- WebSockets
- ADO.NET
- PostgreSQL
- Dapper AOT
- SQLite
Those are not supported yet:
- ASP.NET Core MVC
- WebAPI
- SignalR
- Blazor Server,
- Razor Pages,
- Session, Spa
- Entity Framework Core
Conclusion
.NET Native AOT represents a pivotal stride in optimizing .NET applications. This compilation process into native code enhances performance by minimizing reliance on just-in-time (JIT) compilation during runtime. The resulting benefits include faster execution, reduced deployment overhead, and the potential for improved scalability, making it a compelling choice for boosting efficiency and performance in the realm of .NET development.
With .NET 8, Native AOT is already fairly mature and can be used in production. We can certainly hope that Microsoft will continue improving AOT support.
How is this a cutting-edge advancement? It’s the way software has been produced for half a century prior to the introduction on the .NET platform.
Because you get the both of both worlds. Modern language and platform that brings many facilities compared to C/C++ and raw native executable without the need to install the platform on the production machine.
I have to agree with Richard A. Solomon’s comment. We were compiling to native executables as far back as the 1980s.
The concept of .NET was a direct result of the emerging Java environments in the 1990s.
In addition, Microsoft has always had a delusional concept of trying to always make a singular platform be all things to all people. This is why the company has had so many issues with its development environments and operating systems.
First Microsoft tried to dominate the desktop market with its operating system but found its server market getting pinched by Linux. Microsoft also saw Linux as a threat to its desktop market at times. The result of all this was that Microsoft started to take its development environments into ridiculous areas such as with the new .NET Core frameworks that can now be used to develop across multiple platforms. They threw out the baby with the bathwater and removed a lot of what developers had been relying on simply to make it easier for them to develop the new frameworks.
The question is,how many Microsoft developers are really developing their applications to run on Linux servers? Forget mobile as it became over saturated years ago making actual application development for such devices less of an imperative.
It would be interesting to see the statistics for how the .NET Core Frameworks are really being used.
As a result, there was probably little need to develop a non-native compilation framework in the first place since natively compiled applications have always been historically far more efficient and faster that interpretive style environments that both Java and .NET are.
This is why I have been looking at Python and PHP as I becoming concerned that the Visual Studio environment will eventually be subsumed by its own weight and the internal politics of Microsoft, the results of which don’t appear to make much sense.
Your statement here is wrong:
“On the other hand, .NET Native AOT compilation consists of a single step. Compiling the C# source code into native code on the developer’s machine.”
From the MS website: https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=net7%2Cwindows
“The Native AOT deployment model uses an ahead-of-time compiler to compile IL to native code at the time of publish.”
In short it doesn’t skip the IL step, it uses the JIT to still compile the code. Its just pre-compiled IL -> JIT -> Native.
“Because you get the both of both worlds”
— All AOT langs can have a JIT after the fact. Doing this in the reverse is actually the wrong approach. A lang should be AOT first (for portability) then come with a JIT second (for advanced code-gen features etc).
What .NET has done here is put themselves into a complex box designing a lang around a JIT instead of statically compiler. This ends up with the C# lang crippled when it comes to improving lang design down the road as its now held back by IL shortcomings. The object system being a huge one when it comes to structs.
>In short it doesn’t skip the IL step, it uses the JIT to still compile the code. Its just pre-compiled IL -> JIT -> Native.
please consider reading an entire section before commenting thanks, this is an implementation details, the section then explains “This process involves transforming the C# code into IL code and subsequently into Native code, forming a two-step compilation process. But this is an implementation detail”
>. This ends up with the C# lang crippled when it comes to improving lang design down the road as its now held back by IL shortcomings. The object system being a huge one when it comes to structs.
Let me disagree, C# is not crippled at all and it was a welcome addition from MS to provide a language with a smart runtime in 2001. Now they address some specific scenarios with AOT but it won’t be the future for all .NET app