Questa pagina può contenere testo tradotto automaticamente.

Prestazioni Native AOT in C#

Quanto sono veloci le applicazioni .NET Native AOT rispetto al codice gestito normale? Native 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. Prima esamineremo i benchmark ufficiali di Microsoft. Consentono di confrontare diverse opzioni di distribuzione .NET per semplici applicazioni ASP.NET.

Poi, imparerai come eseguire benchmark propri usando i tool BenchmarkDotNet e hyperfine. Questi benchmark consentono di misurare la velocità del codice nel tuo ambiente.

Benchmark ASP.NET

Il team ASP.NET mantiene un'infrastruttura solida per i test di prestazioni. Verificano vari scenari in ambienti diversi.

Siamo particolarmente interessati ai benchmark Native AOT. La fonte principale delle informazioni è la seguente dashboard PowerBI. I dati lì sono basati 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 applicazione:

  • Stage1 - una minimal API basata su HTTP e JSON. Il codice sorgente dell'applicazione si trova in /src/BenchmarksApps/BasicMinimalApi.
  • Stage1Grpc - una API simile basata su gRPC (/src/BenchmarksApps/Grpc/BasicGrpc)
  • Stage2 - applicazione web completa con database e autenticazione (/src/BenchmarksApps/TodosApi)

Scenari di distribuzione .NET

Le applicazioni di test vengono eseguite in ambienti diversi. Al momento, i benchmark usano macchine virtuali Windows e Linux con 28 core. Esistono anche ambienti Linux separati per processori ARM e Intel.

Le applicazioni vengono testate anche in configurazioni diverse. Un'applicazione in una certa configurazione definisce uno "scenario".

Puoi tenere premuto il tasto Ctrl (o ⌘) per selezionare più scenari o ambienti nella dashboard PowerBI.

Metriche

I benchmark raccolgono metriche fondamentali per ogni applicazione distribuita. Ad esempio, i test misurano il numero di richieste al secondo (RPS), il tempo di avvio, il working set massimo della memoria.

Questo ci consente di confrontare i valori delle metriche per varie configurazioni della stessa applicazione.

Confronto delle prestazioni

Confronteremo gli scenari StageX con StageXAot e StageXAotSpeedOpt. Usano 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 usano anche la variabile di ambiente DOTNET_GCDynamicAdaptationMode=1.

Gli scenari StageXAotSpeedOpt consentono di stimare l'impatto dell'impostazione OptimizationPreference = Speed.

Puoi esaminare anche gli scenari StageXTrimR2RSingleFile. Tali scenari corrispondono a una distribuzione ReadyToRun con trimming, che è un'altra forma di compilazione ahead-of-time 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ù rapidamente delle versioni gestite. Questo è vero sia per le applicazioni Stage1 sia per Stage2, e per tutti gli ambienti. Risultati di esempio:

Scenario Tempo di avvio (ms)
Stage2AotSpeedOpt 100
Stage2Aot 109
Stage2 528

Working set

Il working set massimo per le applicazioni Native AOT è inferiore rispetto alle versioni gestite. Su Linux, le versioni gestite usano circa 1,5 - 2 volte più RAM delle versioni AOT. Ad esempio:

Scenario Working set massimo (MB)
Stage1Aot 56
Stage1AotSpeedOpt 57
Stage1 126

Su Windows, la differenza è minore. In particolare, per Stage2:

Scenario Working set massimo (MB)
Stage2Aot 152
Stage2AotSpeedOpt 150
Stage2 167

Richieste al secondo

Valori RPS più alti indicano un'applicazione più veloce. La leggera applicazione Stage1 gestisce in genere circa 800-900K richieste al secondo. La più grande applicazione Stage2 gestisce solo circa 200K richieste.

Per l'applicazione Stage2, la versione .NET gestisce più richieste delle versioni AOT in tutti gli ambienti. La velocità della versione Stage2AotSpeedOpt a volte è vicina. Ma, in genere, 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 supera la versione gestita. Risultati di esempio da Ampere Linux:

Scenario RPS
Stage1AotSpeedOpt 929,524
Stage1Aot 912,344
Stage1 844,659

Quindi, l'ambiente e il codice dell'applicazione possono influire in modo significativo sulla velocità. Ha senso eseguire benchmark propri per stimare i vantaggi di Native AOT per il tuo progetto. Scriviamo benchmark personalizzati senza l'infrastruttura di test Microsoft.

Benchmark delle applicazioni Native AOT

Useremo 2 tipi di benchmark. Il primo si basa su BenchmarkDotNet, la libreria popolare per il benchmarking del codice .NET. Questi benchmark confrontano la velocità pura, escludendo il tempo di avvio.

Il secondo si basa sul tool 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 diagnoser NativeMemoryProfiler in BenchmarkDotNet non supporta il runtime Native AOT. Anche hyperfine attualmente non traccia l'uso della memoria.

Puoi scaricare il codice sorgente dal repository NativeAotBenchmarks su GitHub. Ti invitiamo a provarlo nel tuo ambiente. Questo articolo descrive risultati ottenuti su un portatile Windows 11 con processore Intel Core i9-13900H e 16 GB di RAM.

Assicurati di eseguire correttamente i benchmark. Ecco le raccomandazioni comuni:

  • Usa la build Release.
  • Chiudi tutte le applicazioni tranne il processo di benchmark. Ad esempio, disabilita l'antivirus, chiudi Visual Studio e un browser web.
  • Tieni il portatile collegato all'alimentazione e usa la modalità di prestazioni migliori.
  • Usa gli stessi dati di input negli scenari confrontati.

Casi di test

Benchmarkeremo 2 scenari in .NET 8:

1. Codice C# semplice per la compressione di una stringa usando il conteggio dei 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 di conversione da PDF a PNG che usa Docotic.Pdf.

Prerequisiti

Installa i 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 nella 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 warmup. Questo aiuta JIT a precompilare il codice e a raccogliere alcune statistiche. Quindi, tali benchmark escludono il tempo di avvio dal confronto.

Compressione di stringhe

Il benchmark CompressString per la compressione di stringhe usa una stringa lunga con caratteri duplicati. L'errore comune sarebbe 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 devi inizializzare un generatore casuale con lo stesso seed.

La versione Native AOT è circa 1,08 volte più veloce della versione .NET 8:

Metodo Runtime Media Errore Deviazione standard
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 a PNG

I benchmark da PDF a PNG elaborano i documenti PDF in memoria. Questo consente di escludere l'interazione con il file system. Le operazioni di I/O su 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 di elaborazione. La versione .NET 8 è leggermente più veloce per questo documento:

Metodo Runtime Media Errore Deviazione standard
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. Viene convertito in un PNG a 300 dpi. E la velocità è quasi uguale:

Metodo Runtime Media Errore Deviazione standard
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 usa l'impostazione OptimizationPreference=Speed. Puoi abilitarla in NativeAotTestApp.csproj: <OptimizationPreference>Speed</OptimizationPreference>

Usa lo script benchmark.bat per eseguire i 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. Poi ne confronta le prestazioni con comandi simili: hyperfine --warmup 3 "net8-app.exe" "native-aot-app.exe"

Le esecuzioni di warmup in hyperfine aiutano ad avviare le applicazioni di test con cache disco "calde". A differenza di BenchmarkDotNet, il warmup di 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 numero di iterazioni. Consente di ripetere lo stesso codice più volte in un semplice ciclo:

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

L'idea è 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 i benchmark con una sola iterazione. Una versione Native AOT è molto più veloce. Poi esegui gli stessi benchmark con più iterazioni e la velocità totale di entrambe le versioni diventa uguale. Significa che, dopo l'avvio, la versione gestita è in realtà più veloce.

Compressione di stringhe

Per 100.000 iterazioni della stessa compressione di stringa in 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 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 a PNG

Per una singola iterazione della conversione da PDF a PNG di Banner Edulink One.pdf, 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ù rapidamente rispetto a .NET normale. I benchmark ufficiali mostrano anche che le applicazioni AOT hanno footprint di memoria più piccoli.

Ma dopo l'avvio, le applicazioni gestite in genere mostrano una velocità migliore. Questo accade perché JIT ha accesso alle informazioni di runtime. Nelle applicazioni di lunga durata, può rigenerare codice più efficace basato su ottimizzazione guidata dal profilo dinamico e altre tecniche.

I benchmark ASP.NET consentono di confrontare diverse configurazioni dal punto di vista delle prestazioni. Tuttavia, i risultati dipendono dal sistema operativo e dall'architettura del processore. Devi eseguire benchmark propri nel tuo ambiente di destinazione per trovare la configurazione di distribuzione ottimale.