Cette page peut contenir du texte traduit automatiquement.

Performances Native AOT en C#

À quelle vitesse les applications .NET Native AOT se comparent-elles au code managé standard ? Native AOT peut-il surpasser JIT ? Comment mesurer les performances des applications Native AOT ?

Comparaison des performances Native AOT

Cet article fait partie d'une série sur Native AOT dans .NET. Si vous ne connaissez pas Native AOT, lisez d'abord la partie Comment développer des applications Native AOT dans .NET.

Cet article compare les performances de .NET et de Native AOT. D'abord, nous examinerons les benchmarks officiels de Microsoft. Ils permettent de comparer différentes options de déploiement .NET pour des applications ASP.NET simples.

Ensuite, vous apprendrez à exécuter vos propres benchmarks à l'aide de BenchmarkDotNet et des outils hyperfine. De tels benchmarks permettent de mesurer la vitesse du code dans votre environnement.

Benchmarks ASP.NET

L'équipe ASP.NET maintient une infrastructure solide pour les tests de performance. Elle teste divers scénarios dans différents environnements.

Les benchmarks Native AOT sont ceux qui nous intéressent le plus. La source principale d'informations est le tableau de bord PowerBI suivant. Les données qui s'y trouvent reposent sur 3 « piliers » : les applications de test, les scénarios de déploiement et les métriques.

Applications de test

Vous pouvez trouver le code source des benchmarks et des applications de test dans le dépôt aspnet/Benchmarks.

Les benchmarks Native AOT comparent 3 types d'applications :

  • Stage1 - une API minimale basée sur HTTP et JSON. Le code source de l'application se trouve dans /src/BenchmarksApps/BasicMinimalApi.
  • Stage1Grpc - une API similaire basée sur gRPC (/src/BenchmarksApps/Grpc/BasicGrpc)
  • Stage2 - une application web complète impliquant une base de données, l'authentification (/src/BenchmarksApps/TodosApi)

Scénarios de déploiement .NET

Les applications de test sont exécutées dans différents environnements. À l'heure actuelle, les benchmarks utilisent des machines virtuelles Windows et Linux avec 28 cœurs. Il existe aussi des environnements Linux distincts pour les processeurs ARM et Intel.

Les applications sont également testées dans différentes configurations. Une application dans une certaine configuration définit un « scénario ».

Vous pouvez maintenir la touche Ctrl (ou ⌘) enfoncée pour sélectionner plusieurs scénarios ou environnements sur le tableau de bord PowerBI.

Métriques

Les benchmarks collectent des métriques fondamentales pour chaque application déployée. Par exemple, les tests mesurent le nombre de requêtes par seconde (RPS), le temps de démarrage et le working set maximal.

Cela nous permet de comparer les valeurs des métriques pour diverses configurations d'une même application.

Comparaison des performances

Nous comparerons les scénarios StageX avec StageXAot et StageXAotSpeedOpt. Ils utilisent la configuration suivante :

Scenario dotnet publish build arguments
StageX PublishAot=false
EnableRequestDelegateGenerator=false
Stage2Aot PublishAot=true
StripSymbols=true
Stage2AotSpeedOpt PublishAot=true
StripSymbols=true
OptimizationPreference=Speed

Tous les scénarios ci-dessus utilisent également la variable d'environnement DOTNET_GCDynamicAdaptationMode=1.

Les scénarios StageXAotSpeedOpt permettent d'estimer l'impact du paramètre OptimizationPreference = Speed.

Vous pouvez aussi examiner les scénarios StageXTrimR2RSingleFile. De tels scénarios correspondent à un déploiement ReadyToRun avec trimming, qui est une autre forme de compilation anticipée dans .NET. Parfois, c'est une bonne alternative à Native AOT.

Voici les résultats actuels de comparaison des performances pour .NET 9 Release Candidate (septembre 2024) :

Temps de démarrage

Les applications AOT démarrent beaucoup plus vite que les versions managées. C'est vrai pour les applications Stage1 et Stage2, et pour tous les environnements. Exemples de résultats :

Scenario Startup time (ms)
Stage2AotSpeedOpt 100
Stage2Aot 109
Stage2 528

Working set

Le working set maximal des applications Native AOT est inférieur à celui des versions managées. Sous Linux, les versions managées utilisent environ 1,5 à 2 fois plus de RAM que les versions AOT. Par exemple :

Scenario Max working set (MB)
Stage1Aot 56
Stage1AotSpeedOpt 57
Stage1 126

Sous Windows, l'écart est plus faible. En particulier pour Stage2 :

Scenario Max working set (MB)
Stage2Aot 152
Stage2AotSpeedOpt 150
Stage2 167

Requêtes par seconde

Des valeurs RPS plus élevées signifient une application plus rapide. L'application légère Stage1 gère généralement environ 800 à 900 K requêtes par seconde. L'application plus lourde Stage2 ne gère qu'environ 200 K requêtes.

Pour l'application Stage2, la version .NET traite plus de requêtes que les versions AOT dans tous les environnements. La vitesse de la version Stage2AotSpeedOpt est parfois proche. Mais, en général, elle se situe entre Stage2 et Stage2Aot. Voici les résultats typiques :

Scenario RPS
Stage2 235,008
Stage2AotSpeedOpt 215,637
Stage2Aot 194,264

Les résultats pour l'application Stage1 sont similaires sous Intel Linux et Intel Windows. Cependant, sous Ampere Linux, AOT bat la version managée. Exemples de résultats sous Ampere Linux :

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

Ainsi, l'environnement et le code de l'application peuvent affecter significativement la vitesse. Il est pertinent d'exécuter vos propres benchmarks pour estimer les bénéfices de Native AOT pour votre projet. Écrivons des benchmarks personnalisés sans l'infrastructure de test Microsoft.

Mesure des performances des applications Native AOT

Nous utiliserons 2 types de benchmarks. Le premier repose sur BenchmarkDotNet - la bibliothèque populaire pour mesurer les performances du code .NET. Ces benchmarks comparent la vitesse pure, hors temps de démarrage.

Le second repose sur l'outil hyperfine. Il permet de comparer le temps d'exécution de deux commandes shell. Ces benchmarks comparent la vitesse globale, y compris le temps de démarrage.

Nous ne comparerons pas la consommation mémoire ici. À l'heure actuelle, le diagnostiqueur NativeMemoryProfiler de BenchmarkDotNet ne prend pas en charge le runtime Native AOT. hyperfine ne suit pas non plus l'utilisation mémoire pour le moment.

Vous pouvez télécharger le code source depuis le dépôt NativeAotBenchmarks sur GitHub. Nous vous encourageons à les essayer dans votre environnement. Cet article décrit des résultats obtenus sur un ordinateur portable Windows 11 équipé d'un processeur Intel Core i9-13900H et de 16 Go de RAM.

Assurez-vous d'exécuter les benchmarks correctement. Voici les recommandations courantes :

  • Utilisez la version Release.
  • Fermez toutes les applications, sauf le processus de benchmark. Par exemple, désactivez l'antivirus, fermez Visual Studio et un navigateur web.
  • Gardez votre ordinateur portable branché et utilisez le mode de performance maximale.
  • Utilisez les mêmes données d'entrée dans les scénarios comparés.

Cas de test

Nous mesurerons 2 scénarios dans .NET 8 :

1. Un code C# simple de compression de chaîne utilisant les comptes de caractères répétés. Par exemple, la chaîne "aabcccccaaa" deviendrait "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. Une tâche plus lourde de conversion PDF en PNG qui utilise Docotic.Pdf.

Prérequis

Installez les prérequis pour le déploiement .NET Native AOT.

Installez hyperfine pour exécuter les benchmarks correspondants.

Pour les benchmarks PDF vers PNG, obtenez une clé de licence gratuite à durée limitée sur la page Télécharger la bibliothèque PDF C# .NET. Vous devez appliquer la clé de licence dans Helper.cs.

BenchmarkDotNet

Ces benchmarks se trouvent dans le projet NativeAotBenchmarks. Nous comparons les résultats pour RuntimeMoniker.NativeAot80 et RuntimeMoniker.Net80. Par défaut, BenchmarkDotNet compile le code Native AOT avec le paramètre OptimizationPreference=Speed.

BenchmarkDotNet effectue 6 itérations d'échauffement ou plus. Cela aide JIT à précompiler le code et à collecter certaines statistiques. Ainsi, de tels benchmarks excluent le temps de démarrage de la comparaison.

Compression de chaîne

Le benchmark CompressString pour la compression de chaîne utilise une longue chaîne contenant des caractères dupliqués. L'erreur courante serait de générer une chaîne aléatoire. Dans un tel cas, les benchmarks pour Native AOT et .NET 8 utiliseraient des chaînes d'entrée différentes. Il est possible d'utiliser des chaînes aléatoires, mais vous devez initialiser un générateur aléatoire avec la même graine.

La version Native AOT s'exécute environ 1,08 fois plus vite que la version .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 vers PNG

Les benchmarks PDF vers PNG traitent des documents PDF en mémoire. Cela permet d'exclure l'interaction avec le système de fichiers. Les opérations d'E/S avec un disque peuvent biaiser les résultats des benchmarks.

Nous testons la vitesse avec deux documents PDF. Le premier, Banner Edulink One.pdf, est plus complexe. Il est converti en PNG 72 dpi et nécessite plus de temps de traitement. La version .NET 8 est légèrement plus rapide pour ce document :

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

Le deuxième document est plus petit et plus simple. Il est converti en PNG 300 dpi. Et la vitesse est presque identique :

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

Ces benchmarks se trouvent dans le projet NativeAotTestApp. Le projet n'utilise pas le paramètre OptimizationPreference=Speed. Vous pouvez l'activer dans NativeAotTestApp.csproj : <OptimizationPreference>Speed</OptimizationPreference>

Utilisez le script benchmark.bat pour exécuter les tests sous Windows. Vous pouvez le convertir en Bash pour les systèmes d'exploitation Unix/Linux. Le script génère les versions .NET 8 et Native AOT de la même application. Ensuite, il compare leurs performances avec des commandes similaires : hyperfine --warmup 3 "net8-app.exe" "native-aot-app.exe"

Les exécutions d'échauffement dans hyperfine aident à lancer les applications de test avec des caches disque « chauds ». Contrairement à BenchmarkDotNet, l'échauffement d'hyperfine n'aide pas JIT. Par conséquent, les benchmarks hyperfine comparent la vitesse totale de l'application, y compris le temps de démarrage.

Notre application de test prend en charge l'argument de nombre d'itérations. Cela permet de répéter le même code plusieurs fois dans une boucle simple :

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

L'idée est de réduire l'impact de la différence de temps de démarrage. La répétition du même code donne à JIT la possibilité de collecter davantage de statistiques d'exécution et de générer un code plus rapide.

Une situation courante est la suivante. La première fois, vous exécutez les benchmarks avec une seule itération. Une version Native AOT fonctionne beaucoup plus vite. Puis, vous exécutez les mêmes benchmarks avec plusieurs itérations et la vitesse totale des deux versions devient identique. Cela signifie qu'après le démarrage, une version managée est en réalité plus rapide.

Compression de chaîne

Pour 100 000 itérations de la même compression de chaîne en entrée, les performances Native AOT sont meilleures :

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

Mais la vitesse devient presque identique pour 10 000 000 d'itérations :

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 vers PNG

Pour une seule itération de conversion de Banner Edulink One.pdf en PNG, la version AOT s'exécute environ 1,88 fois plus vite que la version .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

Pour 20 itérations, la différence de vitesse est négligeable :

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

Pour 3BigPreview.pdf, la version Native AOT est plus rapide même avec 100 itérations :

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

Conclusion

Les applications Native AOT démarrent plus vite que le .NET classique. Les benchmarks officiels montrent aussi que les applications AOT ont une empreinte mémoire plus faible.

Mais après le démarrage, les applications managées affichent généralement de meilleures performances. Cela s'explique par le fait que JIT a accès aux informations d'exécution. Dans les applications de longue durée, il peut régénérer un code plus efficace à partir de l'optimisation dynamique guidée par le profil et d'autres techniques.

Les benchmarks ASP.NET vous permettent de comparer différentes configurations du point de vue des performances. Cependant, les résultats dépendent du système d'exploitation et de l'architecture du processeur. Vous devez exécuter vos propres benchmarks dans votre environnement cible pour trouver la configuration de déploiement optimale.