I've been playing with the .NET's native AOT (Ahead of Time Compilation)( https://github.com/dotnet/runtimelab/tree/feature/NativeAOT#readme) support to get a better understanding of the implications for .NET libraries and applications that want to take advantage of it. #dotnet
The promise of AOT is that you trade off some compile time performance and dynamism for a system that can optimize for reduced output size, startup speed and improved steady state throughput.
.NET has had lots of different versions of AOT over the years (ngen, crossgen, ready to run). Those versions of AOT always run with a JIT fallbacks so binaries carry both the native compiled code *and* the IL as a fallback that the JIT can use to further optimize.
These modes are all hybrid and have various tradeoffs as a result. The Native AOT in dotnet/runtimelab is true AOT in a sense that there is no dynamic code fallback and is meant to support running in environments where there can't be any dynamic code generation
Why? Small output binaries, improved startup time, ability to run in restricted environments (No JIT), better code generation in cases where the entire program can be analyzed. This is great for immutable deployment environments like containers.
Why would you care about startup time? Command line tools are a big scenario for this. They aren't long running applications so you want them to start extremely quickly without having to warm up. Serverless functions is another place where cold start matters a lot.
Client applications that want to startup quickly (notepad, vscode, etc). You'd love it if these applications avoided any repeated work on every application start, especially if it doesn't change.
This also matters for Blazor applications that want to compile directly to WASM. The underlying runtime is different but the principles of Native AOT are the same. If you want to save on size, then it's a problem carrying both the native code and managed code.
So what does it mean for your code? Well we want to make sure your code doesn't break. It would be ideal if we could take any piece of managed code, compile it to native and hit it "work". You may not see the size benefits but it won't break.
Then to get the size benefits we need to start describing the "dynamic behavior" of your application "statically". This is why people say that reflection is AOT unfriendly. It's really about the linking process that tries to remove unused code to give you the size benefits.
If we look at the other existing AOT solutions for .NET, they have about a 3x size penalty. You're trading off binary size for startup performance. FYI, .NET itself is natively compiled, if it wasn't it would be super slow to JIT all of it on every application startup.
So to avoid this 3x size bloat, we want to
- Get rid of the IL. There's no JIT so no need for the IL to be there.
- Remove metadata for things we don't reflect on.
- Remove unused code. To do this with confidence, we need a way to describe reflection to the linker.
- Warn if the linker can't figure things out
- Optimize the crap out of the remaining code
...
- Profit!
So what things are off the table? Well things that end up needing new code (IL) at runtime. For example, loading new assemblies or generating new code using reflection emit. If new IL instructions are added at runtime, there's nothing to execute it.
This means APIs like
- Assembly.Load
- Array.CreateInstance,
- Type.MakeGenericType.

Are *potentially* unsafe. Making new types at runtime that the AOT compiler didn't see at compile time is problematic.
These APIs are pretty low level but they are used in some key subsystems in .NET namely:
- Serializers
- Dependency injection containers
- RPC systems
- ORMs
- AutoMapper
etc

It's why this post by @marcgravell is so relevant https://twitter.com/marcgravell/status/1389262639260372995?s=20
It's widespread in .NET
The other pattern that is anti-AOT are plugin systems. Systems that load assemblies to discovery new runtime behavior are inherently incompatible with this model. This is one of the things .NET is really good at that systems like golang are not good at.
In golang, you'll see lots of out of process extensibility via gRPC instead of loading dynamically linked libraries in process. This obviously is quite a bit more painful than the typical plugin models .NET developers are used to but it's one of the tradeoffs that need to be made
Remember that thing I said about new code not working? That's only partially true. It could in theory be interpreted at runtime and we have various subsystems that ship with an interpreter.
Expression tree compilation for example has a built in interpreter so it still "works". The problem is that its often slower than just using reflection, especially since Native AOT has "faster reflection".
Right now, we have a multi-stage plan for .NET that includes:
- Making .NET itself linker friendly.
- Making .NET apps/libraries linker friendly.
- Making .NET apps/libraries single file friendly.
- Making .NET apps/libraries AOT friendly.
Currently a lot of this work is being driven by Blazor but the overall effort will accrue towards making the .NET ecosystem better in more environments.
If you care about this stuff, I recommend following @MStrehovsky. I've learnt a lot from him in this space and you will too if you follow.
You can follow @davidfowl.
Tip: mention @twtextapp on a Twitter thread with the keyword “unroll” to get a link to it.

Latest Threads Unrolled: