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 werden Native AOT-Anwendungen verglichen?

Native AOT Leistungsvergleich

Dieser Artikel ist Teil einer Serie über Native AOT in .NET. Wenn Sie mit Native AOT nicht vertraut sind, lesen Sie zuerst den Teil So entwickeln Sie Native AOT-Anwendungen in .NET.

Dieser Artikel vergleicht die Leistung von .NET und Native AOT. Zunächst werden wir die offiziellen Microsoft-Benchmarks überprüfen. Sie ermöglichen den Vergleich verschiedener .NET-Bereitstellungsoptionen für einfache ASP.NET-Anwendungen.

Anschließend lernen Sie, wie Sie mit BenchmarkDotNet und Hyperfine-Tools eigene Benchmarks ausführen. Mit solchen Benchmarks können Sie die Codegeschwindigkeit in Ihrer Umgebung messen.

ASP.NET-Benchmarks

Das ASP.NET-Team unterhält eine solide Infrastruktur für Leistungstests. Sie testen 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.

Testanwendungen

Den Quellcode von Benchmarks und Testanwendungen finden Sie im Repository aspnet/Benchmarks.

Native AOT-Benchmarks vergleichen 3 Anwendungstypen:

  • Stage1 - eine minimale API basierend auf HTTP und JSON. Der Quellcode der Anwendung befindet sich in /src/BenchmarksApps/BasicMinimalApi.
  • Stage1Grpc - eine ähnliche API basierend auf gRPC (/src/BenchmarksApps/Grpc/BasicGrpc)
  • Stage2 - vollständige Webanwendung mit Datenbank, Authentifizierung (/src/BenchmarksApps/TodosApi)

.NET-Bereitstellungsszenarien

Testanwendungen werden in unterschiedlichen Umgebungen ausgeführt. Derzeit verwenden Benchmarks virtuelle Windows- und Linux-Maschinen mit 28 Kernen. Es gibt auch 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 mehrere Szenarien oder Umgebungen auf dem PowerBI-Dashboard auszuwählen.

Metriken

Benchmarks erfassen grundlegende Metriken für jede bereitgestellte Anwendung. Tests messen beispielsweise die Anzahl der Anfragen pro Sekunde (RPS), die Startzeit und den maximalen Arbeitsspeicher.

Dadurch können wir Metrikwerte für verschiedene Konfigurationen derselben Anwendung vergleichen.

Leistungsvergleich

Wir werden StageX-Szenarien mit StageXAot und StageXAotSpeedOpt vergleichen. Sie verwenden die folgende Konfiguration:

Szenario dotnet publish-Build-Argumente
StageX PublishAot=false
EnableRequestDelegateGenerator=false
Stage2Aot PublishAot=true
StripSymbols=true
Stage2AotSpeedOpt PublishAot=true
StripSymbols=true
OptimizationPreference=Speed

Alle oben genannten Szenarien verwenden auch die Umgebungsvariable DOTNET_GCDynamicAdaptationMode=1.

StageXAotSpeedOpt-Szenarien ermöglichen es, die Auswirkungen der Einstellung OptimizationPreference = Speed ​​abzuschätzen.

Sie können auch StageXTrimR2RSingleFile-Szenarien überprüfen. Solche Szenarien entsprechen einer getrimmten ReadyToRun-Bereitstellung, einer anderen Form der Ahead-of-Time-Kompilierung in .NET. Manchmal ist es eine gute Alternative zu Native AOT.

Hier sind die aktuellen Leistungsvergleichsergebnisse für den .NET 9 Release Candidate (September 2024):

Startzeit

AOT-Anwendungen starten viel 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 für Native AOT-Anwendungen ist kleiner als für verwaltete Versionen. Unter Linux verwenden verwaltete Versionen etwa 1,5- bis 2-mal mehr RAM als AOT-Versionen. Beispiel:

Szenario Max. Arbeitssatz (MB)
Stage1Aot 56
Stage1AotSpeedOpt 57
Stage1 126

Unter Windows ist der Unterschied geringer. Insbesondere für Stage2:

Szenario Max. Arbeitssatz (MB)
Stage2Aot 152
Stage2AotSpeedOpt 150
Stage2 167

Anfragen pro Sekunde

Höhere RPS-Werte bedeuten eine schnellere Anwendung. Die leichte Stage1-Anwendung verarbeitet normalerweise etwa 800–900.000 Anfragen pro Sekunde. Die größere Stage2-Anwendung verarbeitet nur etwa 200.000 Anfragen.

Für die Stage2-Anwendung verarbeitet die .NET-Version in allen Umgebungen mehr Anfragen als die AOT-Versionen. Die Geschwindigkeit der Stage2AotSpeedOpt-Version ist manchmal ähnlich. Normalerweise 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 unter Intel Linux und Intel Windows ähnlich. Unter Ampere Linux schlägt AOT jedoch die verwaltete Version. Beispielergebnisse von Ampere Linux:

Szenario RPS
Stage1AotSpeedOpt 929.524
Stage1Aot 912.344
Stage1 844.659

Die Umgebung und der Anwendungscode können die Geschwindigkeit also erheblich beeinflussen. Es ist sinnvoll, eigene Benchmarks auszuführen, um die Vorteile von Native AOT für Ihr Projekt abzuschätzen. Schreiben wir benutzerdefinierte Benchmarks ohne die Testinfrastruktur von Microsoft.

Benchmarking von Native AOT-Anwendungen

Wir werden 2 Arten von Benchmarks verwenden. Der erste basiert auf BenchmarkDotNet – der beliebten Bibliothek zum Benchmarking von .NET-Code. Diese Benchmarks vergleichen die reine Geschwindigkeit, ohne Startzeit.

Der zweite basiert auf dem Tool hyperfine. Es ermöglicht den Vergleich der Ausführungszeit von zwei Shell-Befehlen. Diese Benchmarks vergleichen die Gesamtgeschwindigkeit, einschließlich der Startzeit.

Wir werden hier den Speicherverbrauch nicht vergleichen. Derzeit unterstützt der NativeMemoryProfiler-Diagnostiker in BenchmarkDotNet die Native AOT-Laufzeit nicht. Hyperfine verfolgt derzeit auch nicht den Speicherverbrauch.

Sie können den Quellcode aus dem NativeAotBenchmarks-Repository auf GitHub herunterladen. Wir empfehlen Ihnen, sie in Ihrer Umgebung auszuprobieren. Dieser Artikel beschreibt die Ergebnisse eines Windows 11-Laptops mit Intel Core i9-13900H-Prozessor und 16 GB RAM.

Stellen Sie sicher, dass Sie Benchmarks richtig ausführen. Hier sind die allgemeinen Empfehlungen:

  • Verwenden Sie den Release-Build.
  • Schalten Sie alle Anwendungen außer dem Benchmark-Prozess aus. Deaktivieren Sie beispielsweise Antivirensoftware, schließen Sie Visual Studio und einen Webbrowser.
  • Lassen Sie Ihren Laptop angeschlossen und verwenden Sie den Modus mit der besten Leistung.
  • Verwenden Sie in den zu vergleichenden Szenarien dieselben Eingabedaten.

Testfälle

Wir werden 2 Szenarien in .NET 8 vergleichen:

1. Einfacher C#-Code für eine Zeichenfolgenkomprimierung unter Verwendung der Anzahl wiederholter Zeichen. Beispielsweise würde die Zeichenfolge „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 anspruchsvollere PDF-zu-PNG-Konvertierungsaufgabe, die Docotic.Pdf verwendet.

Voraussetzungen

Installieren Sie Voraussetzungen für die .NET Native AOT-Bereitstellung.

Installieren Sie Hyperfine, um entsprechende Benchmarks auszuführen.

Für PDF-zu-PNG-Benchmarks erhalten Sie auf der Seite Laden Sie die C# .NET PDF-Bibliothek herunter 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 erstellt BenchmarkDotNet Native AOT-Code mit der Einstellung OptimizationPreference=Speed.

BenchmarkDotNet führt 6 oder mehr Aufwärmiterationen durch. Das hilft JIT, Code vorzukompilieren und einige Statistiken zu sammeln. Daher schließen solche Benchmarks die Startzeit vom Vergleich aus.

Stringkomprimierung

Der CompressString-Benchmark für die String-Komprimierung verwendet einen langen String mit doppelten Zeichen. Der häufigste Fehler wäre, einen zufälligen String zu generieren. In einem solchen Fall würden Benchmarks für Native AOT und .NET 8 unterschiedliche Eingabestrings verwenden. Es ist möglich, zufällige Strings zu verwenden, 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 kann die Interaktion mit dem Dateisystem ausgeschlossen werden. I/O-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 Zeit für die Verarbeitung. 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 fast 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 nicht die Einstellung OptimizationPreference=Speed. Sie können sie in 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 konvertieren. Das Skript erstellt .NET 8- und Native AOT-Versionen derselben App. Anschließend vergleicht es ihre Leistung mit ähnlichen Befehlen: hyperfine --warmup 3 "net8-app.exe" "native-aot-app.exe"

Warmup-Läufe in Hyperfine helfen dabei, Testanwendungen auf „warmen“ Festplatten-Caches zu starten. Anders als BenchmarkDotNet hilft das Hyperfine-Warmup nicht bei JIT. Daher vergleichen Hyperfine-Benchmarks die Gesamtgeschwindigkeit der Anwendung, einschließlich der Startzeit.

Unsere Testanwendung unterstützt das Argument der Iterationsanzahl. Es ermöglicht, denselben Code mehrmals in einer einfachen Schleife zu wiederholen:

for (int i = 0; i < iterationCount; ++i)
    CompressString(args);

Die Idee besteht darin, die Auswirkungen des Zeitunterschieds beim Start zu verringern. Durch die Wiederholung desselben Codes erhält JIT die Möglichkeit, mehr Laufzeitstatistiken zu sammeln und schnelleren Code zu generieren.

Eine häufige Situation ist die folgende. Beim ersten Mal führen Sie Benchmarks mit einer einzigen Iteration aus. Eine Native AOT-Version arbeitet viel schneller. Dann führen Sie dieselben Benchmarks mit mehreren Iterationen aus und die Gesamtgeschwindigkeit beider Versionen wird gleich. Das bedeutet, dass eine verwaltete Version nach dem Start tatsächlich schneller ist.

Stringkomprimierung

Bei 100.000 Iterationen derselben Eingabezeichenfolgenkomprimierung ist die Leistung von Native AOT 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 die Geschwindigkeit bleibt bei 10.000.000 Iterationen 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 mit 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 im Vergleich zu regulärem .NET schneller. Die offiziellen Benchmarks zeigen auch, dass AOT-Anwendungen weniger Speicherbedarf haben.

Aber nach dem Start zeigen verwaltete Anwendungen normalerweise eine bessere Geschwindigkeit. Das liegt daran, dass JIT Zugriff auf Laufzeitinformationen hat. In Anwendungen mit langer Laufzeit kann es effektiveren Code basierend auf dynamischer profilgesteuerter Optimierung und anderen Techniken neu generieren.

Mit ASP.NET-Benchmarks können Sie verschiedene Konfigurationen aus Leistungssicht vergleichen. Die Ergebnisse hängen jedoch vom Betriebssystem und der Prozessorarchitektur ab. Sie müssen in Ihrer Zielumgebung eigene Benchmarks ausführen, um die optimale Bereitstellungskonfiguration zu finden.