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?

Confronto delle prestazioni di 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.