I'm a huge fan of optimisation and Ahead of Time Compilation (AOT) is right up my alley. Ever since learning about CoreRT, I've wanted to understand and use AOT in .NET. So with AOT becoming baked into .NET 7, I wanted to understand it the best I could before the official .NET 7 release date.
AOT in .NET has looked different over time, with major milestones (to my understanding) being:
- Native AOT Experiment
- .NET 7
I'll be going through each of them and pooling together all the knowledge and sources I've accumulated so far. There's a lot that I've missed I'm sure, but I think this piece is a good crack at an overview.
What is AOT?
But before we dive in, what is Ahead of Time compilation, or AOT?
A general answer from Wikipedia:
In computer science, ahead-of-time compilation (AOT compilation) is the act of compiling an (often) higher-level programming language into an (often) lower-level language before execution of a program, usually at build-time, to reduce the amount of work needed to be performed at run time.
The .NET 7 Preview 3 announcement post has a more in-depth sales pitch:
Ahead-of-time (AOT) compilation refers to an umbrella of technologies which generate code at application build time, instead of run-time.
Existing AOT-compiled .NET assemblies contain platform-specific data structures and native code to frontload work typically done at runtime. Precompiling these artifacts saves time at startup (e.g. ReadyToRun), and enables access to no-JIT platforms (e.g. iOS).
The main advantage of Native AOT is in startup time, memory usage, accessing to restricted platforms (no JIT allowed), and smaller size on disk. Applications start running the moment the operating system pages in them into memory. The data structures are optimized for running AOT generated code, not for compiling new code at runtime. This is similar to how languages like Go, Swift, and Rust compile. Native AOT is best suited for environments where startup time matters the most.
We can oversimplify to: We don't need the JIT.
Want to know some thoughts from an experienced .NET team member? Check out David Fowler's tweets on AOT in .NET:
With some base knowledge, let's dive into the AOT flavours of .NET.
What is CoreRT?
A proper attempt at cross-platform native binaries. At the time (mid 2010s) there was a huge push for both the unification of the .NET Framework APIs and for .NET to have first party cross platform support (thank you Mono for paving the way). Part of this strategy was AOT such that to produce native binaries for OSX, Linux, and Windows.
CoreRT is now unsupported and has been superseded by the Native AOT Experiment.
How was it used?
Take any of the flags and apply them to your
.csproj or via the command line.
An example would be to put the following into your
<PropertyGroup> <IlcInvariantGlobalization>true</IlcInvariantGlobalization> <IlcDisableReflection>true</IlcDisableReflection> <IlcOptimizationPreference>Size</IlcOptimizationPreference> </PropertyGroup>
IlcInvariantGlobalization: The globalization invariant mode that removes code and data that supports non-english cultures. Removing code and data makes your app smaller.
IlcDisableReflection: Completely disables the reflection metadata generation outside of some basic calls. This greatly reduces size of self contained deployments.
IlcOptimizationPreference(size): When generating optimized code, favour smaller code size.
CoreRT was the first time I had heard about AOT in the .NET space. I think it's really cool that there were so many flags and switches to flip to throw away safeties and potential functionality all for the sake of performance/size - like taking off your car doors and removing the floors to go faster.
I really want to draw your attention to the piece that really set off my fire for AOT:
Building a self-contained game in C# under 8 kilobytes - I was in awe when first reading this. It's a console version of the Snake game that is built in .NET and is self contained - i.e. you don't need .NET installed on the target machine because it bundles it up in the
.exe for you. However, as in the title, the thing that is astonishing is that it is 8 kilobytes. Remember, it has the runtime with it so it can run without .NET installed. This is achieved by some extremely clever programming, tool chain tinkering, and CoreRT at the heart.
I ended up doing somewhat of a fan project of my own called TinyWordle where I use the Native AOT Experiment to try out similar results - but more on that later.
- A great history of CoreRT post by Matt Warren
- Intro to CoreRT
- GitHub Documentation
- CoreRT .NET glossary
What is ReadyToRun (R2R)?
Via the Official Documentation:
R2R binaries improve startup performance by reducing the amount of work the just-in-time (JIT) compiler needs to do as your application loads. The binaries contain similar native code compared to what the JIT would produce. However, R2R binaries are larger because they contain both intermediate language (IL) code, which is still needed for some scenarios, and the native version of the same code.
ReadyToRun is a currently supported deployment model. Though it's a weird middle ground as it produces a large output. This is because it uses both native and IL code (to be JITted) bundled together. No true AOT, but certainly a step.
How to Use It
When you are publishing your app, you can use the
-p:PublishReadyToRun=true flag. E.g.:
dotnet publish -c Release -r win-x64 -p:PublishReadyToRun=true
Or you can put it in your
<PropertyGroup> <PublishReadyToRun>true</PublishReadyToRun> </PropertyGroup>
Which will then get automatically picked when doing a
dotnet publish without having to use the flag at the command line.
ReadyToRun has sort of entirely flown under my radar. I've known of it, but never used it myself. It feels like a strange half baked solution - cobbling together unfinished AOT to be selectively used with the normal .NET runtime.
Native AOT Experiment
What is the Native AOT Experiment?
The ahead-of-time (AOT) toolchain can compile .NET application into a native (architecture specific) single-file executable. It can also produce standalone dynamic or static libraries that can be consumed by applications written in other programming languages.
Sounds familiar by this point right? This was essentially a stepping stone between the amazing work in CoreRT and what we'll see in .NET 7.
A lot of what applied with CoreRT applies here - even decent chunks of the documentation were simply copied over, meaning the flags and methodologies are similar. The experiment has graduated into the regular .NET ecosystem and as such is now unsupported.
How to Use it
Similar to CoreRT, pick some flags and apply them to your
<PropertyGroup> <IlcInvariantGlobalization>true</IlcInvariantGlobalization> <IlcDisableReflection>true</IlcDisableReflection> <IlcOptimizationPreference>Speed</IlcOptimizationPreference> </PropertyGroup>
Which does (note: the links while similar to CoreRT, however they will take you to the experimental documentation):
IlcFoldIdenticalMethodBodies: Folds method bodies with identical bytes (method body deduplication). This makes your app smaller, but the stack traces might sometimes look nonsensical
IlcGenerateStackTraceData: Disables generation of stack trace metadata that provides textual names in stack traces.
IlcOptimizationPreference(speed): When generating optimized code, favor code execution speed.
I love it. Experimental AOT walked so .NET 7 could run. I first began to play with this AOT version in my TinyWordle project and did a full writeup on it where I shrunk my self-contained Wordle clone from 62,091 KB to 1,011 KB.
It also lead me to the runtimelab GitHub repo which has all sorts of interesting and spicy experiments going on that might one day graduate into the main .NET runtime.
What is AOT in .NET 7?
The final form of everything above. Creating binaries for specific platforms to have faster startup times and to run in environments without a possible JITter.
As of writing this .NET Preview 5 is officially out, and Preview 7 is unofficially out. And sadly just for now, as Native AOT is easing into the ecosystem, it is limited to console apps and class libraries with the rest coming out in the future.
How to Use it
This one's pretty easy:
<PropertyGroup> <PublishAot>true</PublishAot> </PropertyGroup>
You can also use flags from other deployment models too!
I was super excited when this was announced and perhaps a bit too eager. I threw TinyWordle at Preview 3 and found it wasn't as good as I had hoped - but it got way better in a later preview!. I also threw another project of mine, GameboyColour-Decolouriser, at it and found okay results. However, this is on me because these are literally preview versions and it's only going to get better from here.
I'm super excited.
- .NET 7 AOT Plan
- .NET 7 Preview 2 update
- .NET 7 Preview 3 update
- An amazing writeup with benchmarking across different AOT forms and cloud environments: Native AOT and Ahead-of-Time Compilation in .NET by Mariia K
- .NET 7 Preview experiment: TrimmedTodo by Damian Edwards
Awesome stuff coming out from the .NET team and I'm excited to see where it goes and how I can use it. Like with anything in .NET, there's a lot of ways to do similar things and knowing about AOT is just one of the many deployment tools you have at your disposal.