Skip to main content

A Short Intro to NativeAOT

· 4 min read

Yet another way of shipping C#/.NET code.

What is it?

It's basically C# compiled into native code from the IL, like crossgen2(which it shares code with) might do, except that it's completely different. There is no JIT compiler and IL in the final executable. This has quite a few limitations:

That's just to name a bit. Though (AOT-)compiled regexes can be expected in .NET 7.0 using source generators.

What are the benefits?

Usage

Prerequisites

Arch Linux

Install clang zlib krb5 from official repositories.

Other

Refer to using-nativeaot/prerequisites.md.

To install the NativeAOT package, you'll need to add the nuget.config file like so to the root of your solution or project:

nuget.config
xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
</packageSources>
</configuration>
nuget.config
xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
</packageSources>
</configuration>

Then add the following PackageReference under an ItemGroup in your .csproj like so:

xml
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ILCompiler" Version="7.0.0-*" />
</ItemGroup>
xml
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ILCompiler" Version="7.0.0-*" />
</ItemGroup>

This will use the latest version of the package, so you may sometimes experience a long restore time.

Then, we'll configure some basic-ish options:

xml
<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>Link</TrimMode>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
<DebuggerSupport>false</DebuggerSupport>
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
</PropertyGroup>
xml
<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>Link</TrimMode>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
<DebuggerSupport>false</DebuggerSupport>
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
</PropertyGroup>

You can read more about these options at using-nativeaot/optimizing.md.

Now, when you dotnet publish you'll get a NativeAOT Build. On Linux and MacOS the executables contain quite large debugging symbols, you can use strip to remove them.

Running on Alpine Linux

You can't build specifically for musl yet, but you can build on a glibc system and run on Alpine using some glibc compatibility packages: If you haven't already, enable the community repository and install libstdc++ and gcompat.

Benchmarks

Run time and build size benchmarks done on a hello world app(source code). This doesn't really illustrate real world performance, it's just so you have an idea of the minimal app scenario.

  • contained: Trimmed self-contained build
  • fxdependent: Framework-dependent build - requires .NET installed on system
  • nativeaot: NativeAOT build
  • -r2r: ReadyToRun build with -p:PublishReadyToRun=true

Windows

Windows 11 Dev Build 22538.1000

Run Time

CommandMean [ms]Min [ms]Max [ms]Relative
win-x64-contained-r2r.exe33.3 ± 2.827.840.12.59 ± 0.37
win-x64-contained.exe58.4 ± 4.452.066.24.55 ± 0.64
win-x64-fxdependent-r2r.exe38.8 ± 3.033.646.43.03 ± 0.42
win-x64-fxdependent.exe38.9 ± 2.333.846.63.03 ± 0.40
win-x64-nativeaot.exe12.8 ± 1.510.018.21.00

Build Sizes

NameSizeRatio
win-x64-fxdependent-r2r156.397KB1
win-x64-nativeaot4021.248KB25.7118
win-x64-contained-r2r14014.893KB89.611
win-x64-contained11302.829KB72.2701
win-x64-fxdependent154.859KB0.9902

Linux

Ubuntu 20.04 WSL2

Run Time

CommandMean [ms]Min [ms]Max [ms]Relative
./linux-x64-contained67.8 ± 4.160.880.626.99 ± 3.40
./linux-x64-contained-r2r21.2 ± 3.217.833.18.44 ± 1.58
./linux-x64-fxdependent31.2 ± 3.126.744.712.41 ± 1.84
./linux-x64-fxdependent-r2r30.7 ± 2.127.338.212.22 ± 1.58
./linux-x64-nativeaot2.5 ± 0.32.03.81.00

Build Sizes

NameSizeRatio
linux-x64-nativeaot15801.456KB1
linux-x64-fxdependent-r2r149.501KB0.0095
linux-x64-contained12682.127KB0.8026
linux-x64-fxdependent147.961KB0.0094
linux-x64-nativeaot stripped5437.84KB0.3441
linux-x64-contained-r2r15528.847KB0.9827

Sources