Diese Seite kann automatisch übersetzten Text enthalten.
C# Native AOT-Leistung
Wie schnell sind .NET Native AOT-Anwendungen im Vergleich zum regulären verwalteten Code? Kann AOT JIT übertreffen? Wie benchmarkt man Native AOT-Anwendungen?

Dieser Artikel ist Teil einer Reihe über Native AOT in .NET. Wenn Sie mit Native AOT nicht vertraut sind, lesen Sie zuerst den Teil Wie man Native-AOT-Anwendungen in .NET entwickelt.
Dieser Artikel vergleicht die Leistung von .NET und Native AOT. Zuerst betrachten wir die offiziellen Microsoft-Benchmarks. Sie ermöglichen den Vergleich verschiedener .NET-Bereitstellungsoptionen für einfache ASP.NET-Anwendungen.
Danach lernen Sie, wie Sie eigene Benchmarks mit den Tools BenchmarkDotNet und hyperfine ausführen. Solche Benchmarks ermöglichen es Ihnen, die Codegeschwindigkeit in Ihrer Umgebung zu messen.
ASP.NET benchmarks
Das ASP.NET-Team pflegt eine solide Infrastruktur für Leistungstests. Es testet verschiedene Szenarien in unterschiedlichen Umgebungen.
Am meisten interessieren uns die Native AOT-Benchmarks. Die primäre Informationsquelle ist das folgende PowerBI-Dashboard. Die Daten dort basieren auf 3 „Walen“: Testanwendungen, Bereitstellungsszenarien und Metriken.
Test applications
Den Quellcode der Benchmarks und Testanwendungen finden Sie im Repository aspnet/Benchmarks.
Native AOT-Benchmarks vergleichen 3 Anwendungstypen:
- Stage1 - eine minimale API auf Basis von HTTP und JSON. Der Quellcode der Anwendung befindet sich in
/src/BenchmarksApps/BasicMinimalApi. - Stage1Grpc - eine ähnliche API auf Basis von gRPC (
/src/BenchmarksApps/Grpc/BasicGrpc) - Stage2 - vollständige Web-App mit Datenbank und Authentifizierung (
/src/BenchmarksApps/TodosApi)
.NET-Bereitstellungsszenarien
Testanwendungen werden in verschiedenen Umgebungen ausgeführt. Derzeit verwenden die Benchmarks Windows- und Linux-VMs mit 28 Kernen. Es gibt außerdem separate Linux-Umgebungen für ARM- und Intel-Prozessoren.
Anwendungen werden auch in verschiedenen Konfigurationen getestet. Eine Anwendung in einer bestimmten Konfiguration definiert ein „Szenario“.
Sie können die Strg-Taste (oder ⌘) gedrückt halten, um im PowerBI-Dashboard mehrere Szenarien oder Umgebungen auszuwählen.
Metriken
Benchmarks erfassen grundlegende Metriken für jede bereitgestellte Anwendung. Die Tests messen beispielsweise die Anzahl der Anfragen pro Sekunde (RPS), die Startzeit und den maximalen Arbeitssatz.
So können wir Metrikwerte für verschiedene Konfigurationen derselben Anwendung vergleichen.
Leistungsvergleich
Wir vergleichen die Szenarien StageX mit StageXAot und StageXAotSpeedOpt. Sie verwenden die folgende Konfiguration:
| Szenario | Build-Argumente für dotnet publish |
|---|---|
| StageX | PublishAot=false EnableRequestDelegateGenerator=false |
| Stage2Aot | PublishAot=true StripSymbols=true |
| Stage2AotSpeedOpt | PublishAot=true StripSymbols=true OptimizationPreference=Speed |
Alle obigen Szenarien verwenden außerdem die Umgebungsvariable DOTNET_GCDynamicAdaptationMode=1.
Mit den Szenarien StageXAotSpeedOpt lässt sich die Auswirkung der Einstellung OptimizationPreference = Speed abschätzen.
Sie können sich auch die Szenarien StageXTrimR2RSingleFile ansehen. Solche Szenarien entsprechen einer getrimmten ReadyToRun-Bereitstellung, einer weiteren Form der Ahead-of-Time-Kompilierung in .NET. Manchmal ist das eine gute Alternative zu Native AOT.
Hier sind die aktuellen Ergebnisse des Leistungsvergleichs für .NET 9 Release Candidate (September 2024):
Startup-Zeit
AOT-Anwendungen starten deutlich schneller als verwaltete Versionen. Das gilt sowohl für Stage1- als auch für Stage2-Anwendungen und für alle Umgebungen. Beispielergebnisse:
| Szenario | Startzeit (ms) |
|---|---|
| Stage2AotSpeedOpt | 100 |
| Stage2Aot | 109 |
| Stage2 | 528 |
Arbeitssatz
Der maximale Arbeitssatz von Native AOT-Anwendungen ist geringer als bei verwalteten Versionen. Unter Linux verbrauchen verwaltete Versionen etwa 1,5 - 2-mal mehr RAM als AOT-Versionen. Zum Beispiel:
| Szenario | Maximaler Arbeitssatz (MB) |
|---|---|
| Stage1Aot | 56 |
| Stage1AotSpeedOpt | 57 |
| Stage1 | 126 |
Unter Windows ist der Unterschied kleiner. Besonders bei Stage2:
| Szenario | Maximaler Arbeitssatz (MB) |
|---|---|
| Stage2Aot | 152 |
| Stage2AotSpeedOpt | 150 |
| Stage2 | 167 |
Anfragen pro Sekunde
Höhere RPS-Werte bedeuten eine schnellere Anwendung. Die schlanke Stage1-Anwendung verarbeitet normalerweise etwa 800-900K Anfragen pro Sekunde. Die größere Stage2-Anwendung verarbeitet nur etwa 200K Anfragen.
Für die Stage2-Anwendung verarbeitet die .NET-Version in allen Umgebungen mehr Anfragen als die AOT-Versionen. Die Geschwindigkeit der Version Stage2AotSpeedOpt ist manchmal nahe dran. Meist liegt sie jedoch zwischen Stage2 und Stage2Aot. Hier sind die typischen Ergebnisse:
| Szenario | RPS |
|---|---|
| Stage2 | 235,008 |
| Stage2AotSpeedOpt | 215,637 |
| Stage2Aot | 194,264 |
Die Ergebnisse für die Stage1-Anwendung sind auf Intel Linux und Intel Windows ähnlich. Auf Ampere Linux schlägt AOT jedoch die verwaltete Version. Beispielergebnisse von Ampere Linux:
| Szenario | RPS |
|---|---|
| Stage1AotSpeedOpt | 929,524 |
| Stage1Aot | 912,344 |
| Stage1 | 844,659 |
Daher können Umgebung und Anwendungscode die Geschwindigkeit erheblich beeinflussen. Es lohnt sich, eigene Benchmarks auszuführen, um die Vorteile von Native AOT für Ihr Projekt abzuschätzen. Schreiben wir benutzerdefinierte Benchmarks ohne Microsoft-Testinfrastruktur.
Benchmarking von Native AOT-Anwendungen
Wir verwenden 2 Arten von Benchmarks. Der erste basiert auf BenchmarkDotNet – der populären Bibliothek zum Benchmarking von .NET-Code. Diese Benchmarks vergleichen die reine Geschwindigkeit ohne Startzeit.
Der zweite basiert auf dem Tool hyperfine. Damit lässt sich die Ausführungszeit zweier Shell-Befehle vergleichen. Diese Benchmarks vergleichen die Gesamtgeschwindigkeit einschließlich Startzeit.
Den Speicherverbrauch vergleichen wir hier nicht. Derzeit unterstützt der Diagnoser NativeMemoryProfiler in BenchmarkDotNet Native AOT-Runtime nicht. hyperfine erfasst derzeit ebenfalls keine Speichernutzung.
Den Quellcode können Sie aus dem Repository NativeAotBenchmarks auf GitHub herunterladen. Wir empfehlen Ihnen, ihn in Ihrer Umgebung auszuprobieren. Dieser Artikel beschreibt Ergebnisse von einem Windows-11-Laptop mit Intel Core i9-13900H-Prozessor und 16 GB RAM.
Achten Sie darauf, Benchmarks korrekt auszuführen. Hier sind die allgemeinen Empfehlungen:
- Verwenden Sie den Release-Build.
- Schließen Sie alle Anwendungen außer dem Benchmark-Prozess. Deaktivieren Sie zum Beispiel die Antivirensoftware und schließen Sie Visual Studio sowie einen Webbrowser.
- Lassen Sie den Laptop am Strom und verwenden Sie den Modus mit der besten Leistung.
- Verwenden Sie in den verglichenen Szenarien dieselben Eingabedaten.
Test cases
Wir benchmarken 2 Szenarien in .NET 8:
1. Einfacher C#-Code für eine String-Komprimierung anhand der Anzahl wiederholter Zeichen. Zum Beispiel würde der String "aabcccccaaa" zu "a2b1c5a3" werden:
string Compress(string s)
{
StringBuilder compressed = new(s.Length);
for (int i = 0; i < s.Length; ++i)
{
char c = s[i];
for (int j = i + 1; j <= s.Length; ++j)
{
if (j == s.Length || s[j] != c)
{
compressed.Append(c + $"{j - i}");
i = j - 1;
if (compressed.Length > s.Length)
return s;
break;
}
}
}
if (compressed.Length <= s.Length)
return compressed.ToString();
return s;
}
2. Eine aufwendigere Aufgabe zur PDF-zu-PNG-Konvertierung unter Verwendung von Docotic.Pdf.
Voraussetzungen
Installieren Sie die Voraussetzungen für die Native AOT-Bereitstellung von .NET.
Installieren Sie hyperfine, um die entsprechenden Benchmarks auszuführen.
Für PDF-zu-PNG-Benchmarks erhalten Sie auf der Seite C# .NET-PDF-Bibliothek herunterladen einen kostenlosen, zeitlich begrenzten Lizenzschlüssel. Sie müssen den Lizenzschlüssel in Helper.cs anwenden.
BenchmarkDotNet
Diese Benchmarks befinden sich im Projekt NativeAotBenchmarks. Wir vergleichen die Ergebnisse für RuntimeMoniker.NativeAot80 und RuntimeMoniker.Net80. Standardmäßig baut BenchmarkDotNet Native AOT-Code mit der Einstellung OptimizationPreference=Speed.
BenchmarkDotNet führt 6 oder mehr Warmup-Iterationen aus. Das hilft JIT, Code vorab zu kompilieren und einige Statistiken zu sammeln. Daher schließen solche Benchmarks die Startzeit aus dem Vergleich aus.
String-Komprimierung
Der Benchmark CompressString für die String-Komprimierung verwendet einen langen String mit doppelten Zeichen. Der typische Fehler wäre, einen zufälligen String zu generieren. In so einem Fall würden Benchmarks für Native AOT und .NET 8 unterschiedliche Eingabe-Strings verwenden. Zufällige Strings sind möglich, aber Sie müssen einen Zufallsgenerator mit demselben Seed initialisieren.
Die Native AOT-Version läuft etwa 1,08-mal schneller als die .NET-8-Version:
| Method | Runtime | Mean | Error | StdDev |
|---|---|---|---|---|
| Compress | .NET 8.0 | 4.117 ms | 0.0553 ms | 0.0517 ms |
| Compress | NativeAOT 8.0 | 3.809 ms | 0.0403 ms | 0.0377 ms |
PDF-zu-PNG
PDF-zu-PNG-Benchmarks verarbeiten PDF-Dokumente im Speicher. Dadurch wird die Interaktion mit dem Dateisystem ausgeschlossen. E/A-Operationen mit einer Festplatte können die Benchmark-Ergebnisse verfälschen.
Wir testen die Geschwindigkeit mit zwei PDF-Dokumenten. Das erste, Banner Edulink One.pdf, ist komplexer. Es wird in ein PNG mit 72 dpi konvertiert und benötigt mehr Verarbeitungszeit. Die .NET-8-Version ist für dieses Dokument etwas schneller:
| Method | Runtime | Mean | Error | StdDev |
|---|---|---|---|---|
| Convert | .NET 8.0 | 1.103 s | 0.0156 s | 0.0146 s |
| Convert | NativeAOT 8.0 | 1.167 s | 0.0160 s | 0.0149 s |
Das zweite Dokument ist kleiner und einfacher. Es wird in ein PNG mit 300 dpi konvertiert. Und die Geschwindigkeit ist nahezu gleich:
| Method | Runtime | Mean | Error | StdDev |
|---|---|---|---|---|
| Convert | .NET 8.0 | 290.1 ms | 5.78 ms | 6.88 ms |
| Convert | NativeAOT 8.0 | 288.3 ms | 4.44 ms | 3.94 ms |
hyperfine
Diese Benchmarks befinden sich im Projekt NativeAotTestApp. Das Projekt verwendet die Einstellung OptimizationPreference=Speed nicht. Sie können sie in der NativeAotTestApp.csproj aktivieren: <OptimizationPreference>Speed</OptimizationPreference>
Verwenden Sie das Skript benchmark.bat, um Tests unter Windows auszuführen. Sie können es für Unix/Linux-basierte Betriebssysteme in Bash umwandeln. Das Skript baut .NET-8- und Native-AOT-Versionen derselben Anwendung. Anschließend vergleicht es deren Leistung mit ähnlichen Befehlen: hyperfine --warmup 3 "net8-app.exe" "native-aot-app.exe"
Warmup-Läufe in hyperfine helfen, Testanwendungen mit „warmen“ Disk-Caches zu starten. Im Gegensatz zu BenchmarkDotNet hilft der hyperfine-Warmup JIT nicht. Daher vergleichen hyperfine-Benchmarks die Gesamtgeschwindigkeit der Anwendung einschließlich Startzeit.
Unsere Testanwendung unterstützt das Argument für die Iterationsanzahl. Es ermöglicht, denselben Code in einer einfachen Schleife mehrfach zu wiederholen:
for (int i = 0; i < iterationCount; ++i)
CompressString(args);
Die Idee besteht darin, den Einfluss des Startup-Zeit-Unterschieds zu verringern. Durch Wiederholen desselben Codes erhält JIT die Chance, mehr Laufzeitstatistiken zu sammeln und schnelleren Code zu erzeugen.
Ein typisches Szenario ist das folgende. Beim ersten Mal führen Sie Benchmarks mit einer einzelnen Iteration aus. Eine Native AOT-Version arbeitet deutlich schneller. Dann führen Sie dieselben Benchmarks mit mehreren Iterationen aus, und die Gesamtgeschwindigkeit beider Versionen wird gleich. Das bedeutet, dass die verwaltete Version nach dem Start tatsächlich schneller ist.
String-Komprimierung
Bei 100.000 Iterationen derselben String-Komprimierung ist die Native-AOT-Leistung besser:
Benchmark 1: .NET 8 version (100000 iterations)
Time (mean ± σ): 151.5 ms ± 2.6 ms [User: 32.1 ms, System: 1.6 ms]
Range (min … max): 148.0 ms … 157.5 ms 19 runs
Benchmark 2: Native AOT version (100000 iterations)
Time (mean ± σ): 55.1 ms ± 3.1 ms [User: 15.0 ms, System: 2.1 ms]
Range (min … max): 51.6 ms … 65.9 ms 51 runs
Summary
Native AOT version ran 2.75 ± 0.16 times faster than .NET 8 version
Aber bei 10.000.000 Iterationen ist die Geschwindigkeit fast gleich:
Benchmark 1: .NET 8 version (10000000 iterations)
Time (mean ± σ): 3.984 s ± 0.139 s [User: 2.946 s, System: 0.009 s]
Range (min … max): 3.790 s … 4.182 s 10 runs
Benchmark 2: Native AOT version (10000000 iterations)
Time (mean ± σ): 3.956 s ± 0.041 s [User: 2.848 s, System: 0.004 s]
Range (min … max): 3.888 s … 4.016 s 10 runs
Summary
Native AOT version ran 1.01 ± 0.04 times faster than .NET 8 version
PDF-zu-PNG
Bei einer einzelnen Iteration der Konvertierung von Banner Edulink One.pdf in PNG läuft die AOT-Version etwa 1,88-mal schneller als die .NET-8-Version:
Benchmark 1: .NET 8 version (1 iteration)
Time (mean ± σ): 2.417 s ± 0.104 s [User: 1.334 s, System: 0.116 s]
Range (min … max): 2.295 s … 2.629 s 10 runs
Benchmark 2: Native AOT version (1 iteration)
Time (mean ± σ): 1.288 s ± 0.011 s [User: 0.573 s, System: 0.123 s]
Range (min … max): 1.274 s … 1.310 s 10 runs
Bei 20 Iterationen ist der Geschwindigkeitsunterschied vernachlässigbar:
Benchmark 1: .NET 8 version (20 iterations)
Time (mean ± σ): 25.048 s ± 0.223 s [User: 13.278 s, System: 2.312 s]
Range (min … max): 24.751 s … 25.423 s 10 runs
Benchmark 2: Native AOT version (20 iterations)
Time (mean ± σ): 25.213 s ± 0.114 s [User: 12.661 s, System: 2.275 s]
Range (min … max): 25.042 s … 25.350 s 10 runs
Summary
.NET 8 version ran 1.01 ± 0.01 times faster than Native AOT version
Für 3BigPreview.pdf ist die Native AOT-Version sogar bei 100 Iterationen schneller:
Benchmark 1: .NET 8 version (100 iterations)
Time (mean ± σ): 10.009 s ± 0.152 s [User: 5.298 s, System: 0.567 s]
Range (min … max): 9.677 s … 10.189 s 10 runs
Benchmark 2: Native AOT version (100 iterations)
Time (mean ± σ): 8.336 s ± 0.070 s [User: 3.405 s, System: 0.505 s]
Range (min … max): 8.247 s … 8.459 s 10 runs
Summary
Native AOT version ran 1.20 ± 0.02 times faster than .NET 8 version
Abschluss
Native AOT-Anwendungen starten schneller als reguläres .NET. Die offiziellen Benchmarks zeigen außerdem, dass AOT-Anwendungen einen kleineren Speicherbedarf haben.
Nach dem Start zeigen verwaltete Anwendungen jedoch normalerweise die bessere Geschwindigkeit. Das liegt daran, dass JIT Zugriff auf Laufzeitinformationen hat. Bei lang laufenden Anwendungen kann es auf Basis von dynamischer profilgestützter Optimierung und anderen Techniken effektiveren Code neu erzeugen.
Mit den ASP.NET-Benchmarks können Sie verschiedene Konfigurationen aus Leistungssicht vergleichen. Die Ergebnisse hängen jedoch von einem Betriebssystem und einer Prozessorarchitektur ab. Sie müssen eigene Benchmarks in Ihrer Zielumgebung ausführen, um die optimale Bereitstellungskonfiguration zu finden.