Cette page peut contenir du texte traduit automatiquement.

Performances C# Native AOT

Quelle est la vitesse des applications .NET Native AOT par rapport au code managé classique ? AOT peut-il surpasser JIT ? Comment évaluer les applications Native AOT ?

Comparaison des performances de Native AOT

Cet article fait partie d'une série sur Native AOT dans .NET. Si vous n'êtes pas familier avec Native AOT, lisez d'abord la partie Comment développer des applications Native AOT dans .NET.

Cet article compare les performances de .NET et Native AOT. Tout d'abord, nous allons passer en revue 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 tests de performance à l'aide de BenchmarkDotNet et des outils hyperfine. Ces tests de performance vous 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 performances. Ils testent divers scénarios dans différents environnements.

Nous sommes particulièrement intéressés par les benchmarks Native AOT. La principale source d'informations est le tableau de bord PowerBI suivant. Les données qui y sont présentées sont basées sur 3 « baleines » : 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 référentiel aspnet/Benchmarks.

Les tests de performance 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 - application Web complète impliquant une base de données, une authentification (/src/BenchmarksApps/TodosApi)

Scénarios de déploiement .NET

Les applications de test sont exécutées dans différents environnements. Actuellement, les tests utilisent des machines virtuelles Windows et Linux avec 28 cœurs. Il existe également des environnements Linux distincts pour les processeurs ARM et Intel.

Les applications sont également testées dans différentes configurations. Une application dans une configuration donnée 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 tests de performance collectent des mesures 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, la mémoire de travail maximale.

Cela nous permet de comparer les valeurs métriques pour différentes configurations de la même application.

Comparaison des performances

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

Scénario Arguments de construction dotnet publish
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 également consulter les scénarios StageXTrimR2RSingleFile. Ces scénarios correspondent à un déploiement ReadyToRun réduit, qui est une autre forme de compilation anticipée dans .NET. Parfois, c'est une bonne alternative à Native AOT.

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

Temps de démarrage

Les applications AOT démarrent beaucoup plus rapidement que les versions gérées. Cela est vrai pour les applications Stage1 et Stage2 et pour tous les environnements. Exemples de résultats :

Scénario Temps de démarrage (ms)
Stage2AotSpeedOpt 100
Stage2Aot 109
Stage2 528

Ensemble de travail

L'espace de travail maximal pour les applications Native AOT est inférieur à celui des versions gérées. Sous Linux, les versions gérées utilisent environ 1,5 à 2 fois plus de RAM que les versions AOT. Par exemple :

Scénario Ensemble de travail max. (Mo)
Stage1Aot 56
Stage1AotSpeedOpt 57
Stage1 126

Sous Windows, la différence est plus faible. En particulier, pour Stage2 :

Scénario Ensemble de travail max. (Mo)
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 000 requêtes par seconde. L'application Stage2, plus grande, ne gère qu'environ 200 000 requêtes.

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

Scénario RPS
Stage2 235 008
Stage2AotSpeedOpt 215 637
Stage2Aot 194 264

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

Scénario RPS
Stage1AotSpeedOpt 929 524
Stage1Aot 912 344
Stage1 844 659

Ainsi, l'environnement et le code de l'application peuvent affecter considérablement la vitesse. Il est logique d'exécuter vos propres tests pour estimer les avantages de Native AOT pour votre projet. Écrivons des tests personnalisés sans infrastructure de test Microsoft.

Analyse comparative des applications Native AOT

Nous utiliserons 2 types de benchmarks. Le premier est basé sur BenchmarkDotNet - la bibliothèque populaire pour le benchmarking du code .NET. Ces benchmarks comparent la vitesse pure, hors temps de démarrage.

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

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

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

Assurez-vous d'exécuter correctement les tests de performance. Voici les recommandations courantes :

  • Utilisez la version Release.
  • Désactivez toutes les applications à l'exception du processus de test. Par exemple, désactivez le logiciel antivirus, fermez Visual Studio et un navigateur Web.
  • Gardez votre ordinateur portable branché et utilisez le meilleur mode de performance.
  • Utilisez les mêmes données d'entrée dans les scénarios comparés.

Cas de test

Nous allons comparer 2 scénarios dans .NET 8 :

1. Code C# simple pour une compression de chaîne utilisant le nombre 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 conversion PDF en PNG qui utilise Docotic.Pdf.

Prérequis

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

Installez hyperfine pour exécuter les tests de performance correspondants.

Pour les tests de conversion PDF en 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 le fichier Helper.cs.

BenchmarkDotNet

Ces tests se trouvent dans le projet NativeAotBenchmarks. Nous comparons les résultats de RuntimeMoniker.NativeAot80 et RuntimeMoniker.Net80. Par défaut, BenchmarkDotNet génère du code Native AOT avec le paramètre OptimizationPreference=Speed.

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

Compression de chaîne

Le test de performance CompressString pour la compression de chaîne utilise une longue chaîne avec des caractères dupliqués. L'erreur courante serait de générer une chaîne aléatoire. Dans un tel cas, les tests de performance 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 valeur de départ.

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 tests de performance PDF vers PNG traitent les 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 fausser les résultats des tests.

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 égale :

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 tests se trouvent dans le projet NativeAotTestApp. Le projet n'utilise pas le paramètre OptimizationPreference=Speed. Vous pouvez l'activer dans le NativeAotTestApp.csproj : <OptimizationPreference>Speed</OptimizationPreference>

Utilisez le script benchmark.bat pour exécuter des tests sous Windows. Vous pouvez le convertir en Bash pour les systèmes d'exploitation basés sur Unix/Linux. Le script crée 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 de préchauffage dans hyperfine aident à démarrer les applications de test sur des caches de disque « chauds ». Contrairement à BenchmarkDotNet, le préchauffage hyperfine n'aide pas le 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. Il 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 au JIT des chances de collecter plus 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 des tests de performance avec une seule itération. Une version Native AOT fonctionne beaucoup plus rapidement. Ensuite, vous exécutez les mêmes tests de performance avec plusieurs itérations et la vitesse totale des deux versions devient égale. Cela signifie qu'après le démarrage, une version gérée est en fait plus rapide.

Compression de chaîne

Pour 100 000 itérations de la même compression de chaîne d'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 la même 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 rapidement que les applications .NET classiques. Les tests de performance officiels montrent également que les applications AOT ont une empreinte mémoire plus petite.

Mais après le démarrage, les applications gérées affichent généralement une meilleure vitesse. Cela se produit parce que JIT a accès aux informations d'exécution. Dans les applications à exécution longue, il peut régénérer un code plus efficace basé sur l'optimisation guidée par profil dynamique et d'autres techniques.

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