Questa pagina può contenere testo tradotto automaticamente.
Prestazioni C# Native AOT
Quanto sono veloci le applicazioni .NET Native AOT rispetto al normale codice gestito? AOT può superare JIT? Come eseguire il benchmark delle applicazioni Native AOT?
Questo articolo fa parte di una serie su Native AOT in .NET. Se non hai familiarità con Native AOT, leggi prima la parte Come sviluppare applicazioni Native AOT in .NET.
Questo articolo confronta le prestazioni di .NET e Native AOT. Innanzitutto, esamineremo i benchmark ufficiali Microsoft. Consentono di confrontare diverse opzioni di distribuzione .NET per semplici applicazioni ASP.NET.
Quindi, imparerai come eseguire i tuoi benchmark utilizzando gli strumenti BenchmarkDotNet e hyperfine. Tali benchmark ti consentono di misurare la velocità del codice nel tuo ambiente.
Benchmark ASP.NET
Il team ASP.NET mantiene una solida infrastruttura per i test delle prestazioni. Testano vari scenari in diversi ambienti.
Siamo più interessati ai benchmark Native AOT. La fonte primaria di informazioni è la seguente PowerBI dashboard. I dati si basano su 3 "balene": applicazioni di test, scenari di distribuzione e metriche.
Applicazioni di test
Puoi trovare il codice sorgente dei benchmark e delle applicazioni di test nel repository aspnet/Benchmarks.
I benchmark Native AOT confrontano 3 tipi di applicazioni:
- Stage1: un'API minima basata su HTTP e JSON. Il codice sorgente dell'applicazione si trova in
/src/BenchmarksApps/BasicMinimalApi
. - Stage1Grpc: un'API simile basata su gRPC (
/src/BenchmarksApps/Grpc/BasicGrpc
) - Stage2: app Web completa che include database, autenticazione (
/src/BenchmarksApps/TodosApi
)
Scenari di distribuzione .NET
Le applicazioni di test vengono eseguite in ambienti diversi. Al momento, i benchmark utilizzano macchine virtuali Windows e Linux con 28 core. Esistono anche ambienti Linux separati per processori ARM e Intel.
Le applicazioni vengono inoltre testate in diverse configurazioni. Un'applicazione in una configurazione definisce uno "scenario".
Puoi tenere premuto il tasto Ctrl (o ⌘) per selezionare più scenari o ambienti nella dashboard di PowerBI.
Metriche
I benchmark raccolgono metriche fondamentali per ogni applicazione distribuita. Ad esempio, i test misurano il conteggio delle richieste al secondo (RPS), il tempo di avvio, il working set di memoria massima.
Ciò ci consente di confrontare i valori metrici per varie configurazioni della stessa applicazione.
Confronto delle prestazioni
Confronteremo gli scenari StageX con StageXAot e StageXAotSpeedOpt. Utilizzano la seguente configurazione:
Scenario | Argomenti di compilazione dotnet publish |
---|---|
StageX | PublishAot=false EnableRequestDelegateGenerator=false |
Stage2Aot | PublishAot=true StripSymbols=true |
Stage2AotSpeedOpt | PublishAot=true StripSymbols=true OptimizationPreference=Speed |
Tutti gli scenari sopra riportati utilizzano anche la variabile di ambiente
DOTNET_GCDynamicAdaptationMode=1
.
Gli scenari StageXAotSpeedOpt consentono di stimare l'impatto dell'impostazione OptimizationPreference = Speed.
Puoi anche esaminare gli scenari StageXTrimR2RSingleFile. Tali scenari corrispondono alla distribuzione ReadyToRun ridotta, che è un'altra forma di compilazione anticipata in .NET. A volte, è una buona alternativa a Native AOT.
Ecco i risultati attuali del confronto delle prestazioni per .NET 9 Release Candidate (settembre 2024):
Tempo di avvio
Le applicazioni AOT si avviano molto più velocemente delle versioni gestite. Ciò vale sia per le applicazioni Stage1 che Stage2 e per tutti gli ambienti. Risultati di esempio:
Scenario | Tempo di avvio (ms) |
---|---|
Stage2AotSpeedOpt | 100 |
Stage2Aot | 109 |
Stage2 | 528 |
Set di lavoro
Il set di lavoro massimo per le applicazioni Native AOT è inferiore rispetto alle versioni gestite. Su Linux, le versioni gestite utilizzano circa 1,5 - 2 volte più RAM rispetto alle versioni AOT. Ad esempio:
Scenario | Set di lavoro massimo (MB) |
---|---|
Stage1Aot | 56 |
Stage1AotSpeedOpt | 57 |
Stage1 | 126 |
Su Windows, la differenza è minore. In particolare, per Stage2:
Scenario | Set di lavoro massimo (MB) |
---|---|
Stage2Aot | 152 |
Stage2AotSpeedOpt | 150 |
Stage2 | 167 |
Richieste al secondo
Valori RPS più grandi indicano un'applicazione più veloce. L'applicazione Stage1 leggera di solito gestisce circa 800-900K richieste al secondo. L'applicazione Stage2 più grande gestisce solo circa 200K richieste.
Per l'applicazione Stage2, la versione .NET gestisce più richieste rispetto alle versioni AOT in tutti gli ambienti. La velocità della versione Stage2AotSpeedOpt è a volte simile. Ma, di solito, si colloca tra Stage2 e Stage2Aot. Ecco i risultati tipici:
Scenario | RPS |
---|---|
Stage2 | 235.008 |
Stage2AotSpeedOpt | 215.637 |
Stage2Aot | 194.264 |
I risultati per l'applicazione Stage1 sono simili su Intel Linux e Intel Windows. Tuttavia, su Ampere Linux, AOT batte la versione gestita. Risultati campione da Ampere Linux:
Scenario | RPS |
---|---|
Stage1AotSpeedOpt | 929.524 |
Stage1Aot | 912.344 |
Stage1 | 844.659 |
Quindi, l'ambiente e il codice dell'applicazione possono influenzare significativamente la velocità. Ha senso eseguire benchmark personalizzati per stimare i vantaggi di Native AOT per il tuo progetto. Scriviamo benchmark personalizzati senza l'infrastruttura di test Microsoft.
Benchmarking delle applicazioni Native AOT
Utilizzeremo 2 tipi di benchmark. Il primo è basato su BenchmarkDotNet, la popolare libreria per il benchmarking del codice .NET. Questi benchmark confrontano la velocità pura, escluso il tempo di avvio.
Il secondo si basa sullo strumento hyperfine. Consente di confrontare il tempo di esecuzione di due comandi shell. Questi benchmark confrontano la velocità complessiva, incluso il tempo di avvio.
Qui non confronteremo il consumo di memoria. Al momento, il diagnostico NativeMemoryProfiler
in
BenchmarkDotNet non supporta il runtime Native AOT. hyperfine al momento non tiene traccia neanche
dell'utilizzo della memoria.
Puoi scaricare il codice sorgente dal repository NativeAotBenchmarks su GitHub. Ti invitiamo a provarli nel tuo ambiente. Questo articolo descrive i risultati di un laptop Windows 11 con processore Intel Core i9-13900H e 16 Gb di RAM.
Assicurati di eseguire correttamente i benchmark. Ecco i consigli comuni:
- Usa la build Release.
- Disattiva tutte le applicazioni eccetto il processo di benchmark. Ad esempio, disattiva il software antivirus, chiudi Visual Studio e un browser Web.
- Tieni il tuo laptop collegato e usa la modalità con le migliori prestazioni.
- Utilizzare gli stessi dati di input negli scenari confrontati.
Casi di test
Faremo il benchmark di 2 scenari in .NET 8:
1. Semplice codice C# per una compressione di stringa utilizzando i conteggi di caratteri ripetuti. Ad esempio, la stringa "aabcccccaaa" diventerebbe "a2b1c5a3":
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. Un'attività più pesante conversione da PDF a PNG che utilizza Docotic.Pdf.
Prerequisiti
Installa prerequisiti per la distribuzione .NET Native AOT.
Installa hyperfine per eseguire i benchmark corrispondenti.
Per i benchmark da PDF a PNG, ottieni una chiave di licenza gratuita a tempo limitato sulla pagina
Scarica la libreria PDF C# .NET. Devi applicare la chiave di licenza in Helper.cs
.
BenchmarkDotNet
Questi benchmark si trovano nel progetto NativeAotBenchmarks
. Confrontiamo i risultati per
RuntimeMoniker.NativeAot80 e RuntimeMoniker.Net80. Per impostazione predefinita, BenchmarkDotNet
compila il codice Native AOT con l'impostazione OptimizationPreference=Speed
.
BenchmarkDotNet esegue 6 o più iterazioni di riscaldamento. Ciò aiuta JIT a precompilare il codice e raccogliere alcune statistiche. Pertanto, tali benchmark escludono il tempo di avvio dal confronto.
Compressione stringa
Il benchmark CompressString
per la compressione delle stringhe usa una stringa lunga con caratteri
duplicati. L'errore comune sarebbe quello di generare una stringa casuale. In tal caso, i benchmark
per Native AOT e .NET 8 userebbero stringhe di input diverse. È possibile usare stringhe casuali, ma
è necessario inizializzare un generatore casuale con lo stesso seed.
La versione Native AOT è circa 1,08 volte più veloce della versione .NET 8:
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 in PNG
I benchmark PDF to PNG elaborano i documenti PDF in memoria. Ciò consente di escludere l'interazione con il file system. Le operazioni di I/O con un disco possono alterare i risultati dei benchmark.
Testiamo la velocità con due documenti PDF. Il primo, Banner Edulink One.pdf, è più complesso. Viene convertito in un PNG a 72 dpi e richiede più tempo per l'elaborazione. La versione .NET 8 è leggermente più veloce per questo documento:
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 |
Il secondo documento è più piccolo e semplice. È convertito in un PNG a 300 dpi. E la velocità è quasi uguale:
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
Questi benchmark si trovano nel progetto NativeAotTestApp
. Il progetto non utilizza l'impostazione
OptimizationPreference=Speed
. Puoi abilitarla in NativeAotTestApp.csproj:
<OptimizationPreference>Speed</OptimizationPreference>
Utilizza lo script benchmark.bat per eseguire test su Windows. Puoi convertirlo in Bash per sistemi
operativi basati su Unix/Linux. Lo script compila le versioni .NET 8 e Native AOT della stessa app.
Quindi, confronta le loro prestazioni con comandi simili:
hyperfine --warmup 3 "net8-app.exe" "native-aot-app.exe"
Warmup viene eseguito in hyperfine help per avviare applicazioni di test su cache disco "calde". A differenza di BenchmarkDotNet, il warmup hyperfine non aiuta JIT. Pertanto, i benchmark hyperfine confrontano la velocità totale dell'applicazione, incluso il tempo di avvio.
La nostra applicazione di test supporta l'argomento del conteggio delle iterazioni. Consente di ripetere lo stesso codice più volte in un semplice ciclo:
for (int i = 0; i < iterationCount; ++i)
CompressString(args);
L'idea è di ridurre l'impatto della differenza di tempo di avvio. Ripetere lo stesso codice offre a JIT la possibilità di raccogliere più statistiche di runtime e generare codice più veloce.
Una situazione comune è la seguente. La prima volta, esegui benchmark con una singola iterazione. Una versione Native AOT funziona molto più velocemente. Quindi, esegui gli stessi benchmark con più iterazioni e la velocità totale di entrambe le versioni diventa uguale. Ciò significa che dopo l'avvio, una versione gestita è effettivamente più veloce.
Compressione stringa
Per 100.000 iterazioni della stessa compressione di stringa di input, le prestazioni Native AOT sono migliori:
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
Ma la velocità diventa quasi la stessa per 10.000.000 di iterazioni:
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 in PNG
Per una singola iterazione della conversione da Banner Edulink One.pdf a PNG, la versione AOT è circa 1,88 volte più veloce della versione .NET 8:
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
Per 20 iterazioni, la differenza di velocità è trascurabile:
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
Per 3BigPreview.pdf, la versione Native AOT è più veloce anche con 100 iterazioni:
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
Conclusione
Le applicazioni Native AOT si avviano più velocemente rispetto al normale .NET. I benchmark ufficiali mostrano anche che le applicazioni AOT hanno footprint di memoria più piccoli.
Ma dopo l'avvio, le applicazioni gestite solitamente mostrano una velocità migliore. Ciò accade perché JIT ha accesso alle informazioni di runtime. Nelle applicazioni di lunga durata, può rigenerare codice più efficace basato sull'ottimizzazione dinamica guidata dal profilo e altre tecniche.
I benchmark ASP.NET consentono di confrontare diverse configurazioni dal punto di vista delle prestazioni. Tuttavia, i risultati dipendono da un sistema operativo e da un'architettura del processore. È necessario eseguire i propri benchmark nel proprio ambiente di destinazione per trovare la configurazione di distribuzione ottimale.