2023-01-18 09:00:00
Native AOT Overview
This is part of a series on Native AOT.
Top -- Next
In typical .NET development, C# code is compiled to an intermediate language (IL) which is then executed by the Common Language Runtime (CLR). And when we say "executed", we mean that the IL is compiled to native code immediately before it is needed, by a Just in Time (JIT) compiler.
This is how .NET has worked since 2002.
More recently, we are also seeing a different approach, where .NET code is compiled Ahead of Time (AOT), directly to native code, skipping the IL step. The resulting native code doesn't need a runtime (or more precisely, it needs something like a runtime, but which is different and much smaller than the CLR).
One of the early efforts in this area happened with Mono, and the necessity that mothered that invention was iOS, which does not allow JIT compilation for security reasons. So in order to support its strategy of .NET for mobile, Xamarin (later acquired by Microsoft) implemented AOT compilation. The Xamarin toolchain was "ahead of its time" (cheesy pun intended, sorry, not sorry) in its ability to take C# code and create an iOS application that had no JIT.
At Microsoft, a similar effort was underway, but it was based on the .NET Core runtime instead of Mono. This project was called CoreRT, and for several years, it was considered to be "experimental".
Today, CoreRT has been released as part of .NET 7, and the feature is called Native AOT.
Benefits
As mentioned above, one reason for AOT is the need to use .NET on platforms where dynamic code generation is not permitted. The early motivating example was iOS, but WebAssembly has similar constraints.
That said, Microsoft clearly has aspirations for this feature that go beyond environments where JIT is not allowed. AOT has other benefits.
One of the commonly cited advantages of AOT is faster application startup. A program that needs to be JIT compiled at runtime will launch a little bit slower. For a program that gets launched many times, this can add up.
A Native AOT program is typically smaller (in terms of binary size) than the same .NET program built in the traditional manner.
Native AOT allows .NET to use the same compilation model that many other languages use, thus reducing the impedance mismatch for interoperability.
Inherent Limitations
The notion of AOT has certain inherent limitations which are suggested by the name itself. Things need to be done Ahead of Time.
With Native AOT, we are building native code for use with a traditional linker (used by languages such as C++), so we have to follow the rules of that linker. Everything we need must get resolved up front. Once the linker is done, the cockpit door is closed, and nobody else is getting on the plane.
C# and .NET support dynamic code generation at runtime, but those features are not compatible with AOT.
Current limitations
The current implementation of Native AOT is amazing, but it also has plenty of room for future growth. For example, as of .NET 7:
Windows and Linux are supported, but macOS is not.
There is support for static libraries, but it is not yet considered a supported and documented feature.
All dependencies are bundled at build time, which makes things easier, but also less powerful when dealing with libraries.
In addition, a lot of C# code has been written in the last 20 years, and some of it uses features that are not AOT compatible. We should not expect that we can recompile every .NET program unchanged with Native AOT and magically have it Just Work. In some cases, additional effort will be required to make things AOT-compatible.
Examples:
ASP.NET is not yet compatible with AOT, as it uses dynamic code generation under the hood.
Serialization is a reflection-heavy feature that is not AOT compatible.
Still, Native AOT as shipped in .NET 7 is really impressive, and there are plenty of indications that the feature is going to keep getting better in future releases.