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:
- No dynamic code, thus:
- Less platforms supported, for example
linux-arm
(64 bit is supported, while 32-bit is not) - Can't cross-platform build, although you can cross-architecture build which requires it's own load of setting up.
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?
- Faster startup time
- Smaller self-contained executables
- Smaller memory usage
- Can create native libraries
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.configxml
<?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.configxml
<?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 buildfxdependent
: Framework-dependent build - requires .NET installed on systemnativeaot
: NativeAOT build-r2r
: ReadyToRun build with-p:PublishReadyToRun=true
Windows
Windows 11 Dev Build 22538.1000
Run Time
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
win-x64-contained-r2r.exe | 33.3 ± 2.8 | 27.8 | 40.1 | 2.59 ± 0.37 |
win-x64-contained.exe | 58.4 ± 4.4 | 52.0 | 66.2 | 4.55 ± 0.64 |
win-x64-fxdependent-r2r.exe | 38.8 ± 3.0 | 33.6 | 46.4 | 3.03 ± 0.42 |
win-x64-fxdependent.exe | 38.9 ± 2.3 | 33.8 | 46.6 | 3.03 ± 0.40 |
win-x64-nativeaot.exe | 12.8 ± 1.5 | 10.0 | 18.2 | 1.00 |
Build Sizes
Name | Size | Ratio |
---|---|---|
win-x64-fxdependent-r2r | 156.397KB | 1 |
win-x64-nativeaot | 4021.248KB | 25.7118 |
win-x64-contained-r2r | 14014.893KB | 89.611 |
win-x64-contained | 11302.829KB | 72.2701 |
win-x64-fxdependent | 154.859KB | 0.9902 |
Linux
Ubuntu 20.04 WSL2
Run Time
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./linux-x64-contained | 67.8 ± 4.1 | 60.8 | 80.6 | 26.99 ± 3.40 |
./linux-x64-contained-r2r | 21.2 ± 3.2 | 17.8 | 33.1 | 8.44 ± 1.58 |
./linux-x64-fxdependent | 31.2 ± 3.1 | 26.7 | 44.7 | 12.41 ± 1.84 |
./linux-x64-fxdependent-r2r | 30.7 ± 2.1 | 27.3 | 38.2 | 12.22 ± 1.58 |
./linux-x64-nativeaot | 2.5 ± 0.3 | 2.0 | 3.8 | 1.00 |
Build Sizes
Name | Size | Ratio |
---|---|---|
linux-x64-nativeaot | 15801.456KB | 1 |
linux-x64-fxdependent-r2r | 149.501KB | 0.0095 |
linux-x64-contained | 12682.127KB | 0.8026 |
linux-x64-fxdependent | 147.961KB | 0.0094 |
linux-x64-nativeaot stripped | 5437.84KB | 0.3441 |
linux-x64-contained-r2r | 15528.847KB | 0.9827 |