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?

Vergleich der Native AOT-Leistung

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.