Partage via


Étude de cas : Guide du débutant pour optimiser le code et réduire les coûts de calcul (C#, Visual Basic, C++, F#)

Réduire votre temps de calcul signifie réduire les coûts, de sorte que l’optimisation de votre code peut économiser de l’argent. Cette étude de cas utilise un exemple d’application avec des problèmes de performances pour montrer comment utiliser des outils de profilage pour améliorer l’efficacité. Si vous souhaitez comparer les outils de profilage, consultez Quel outil dois-je choisir ?

Cette étude de cas aborde ces sujets :

  • L’importance de l’optimisation du code et son impact sur la réduction des coûts de calcul.
  • Comment utiliser les outils de profilage Visual Studio pour analyser les performances des applications.
  • Comment interpréter les données fournies par ces outils pour identifier les goulots d’étranglement des performances.
  • Comment appliquer des stratégies pratiques pour optimiser le code, en se concentrant sur l’utilisation du processeur, l’allocation de mémoire et les interactions de base de données.

Suivez et appliquez ces techniques à vos propres applications pour les rendre plus efficaces et rentables.

Étude de cas d’optimisation

L’exemple d’application examiné dans cette étude de cas est une application .NET qui exécute des requêtes sur une base de données de blogs et de billets de blog. Il utilise Entity Framework, un ORM populaire (mappageObject-Relational) pour .NET, pour interagir avec une base de données locale SQLite. L’application est structurée pour exécuter un grand nombre de requêtes, simulant un scénario réel où une application .NET peut être nécessaire pour gérer des tâches de récupération de données étendues. L’exemple d’application est une version modifiée de l’exemple de prise en main Entity Framework.

Le principal problème de performances avec l’exemple d’application réside dans la façon dont il gère les ressources de calcul et interagit avec la base de données. L’application a un goulot d’étranglement des performances qui a un impact significatif sur son efficacité et, par conséquent, les coûts de calcul associés à son exécution. Le problème inclut les symptômes suivants :

  • utilisation élevée du processeur: les applications peuvent effectuer des calculs inefficaces ou traiter des tâches d’une manière qui consomme inutilement une grande quantité de ressources processeur. Cela peut entraîner des temps de réponse lents et des coûts opérationnels accrus.

  • Allocation de mémoire inefficace: les applications peuvent parfois rencontrer des problèmes liés à l’allocation et à l'utilisation de la mémoire. Dans les applications .NET, une gestion inefficace de la mémoire peut entraîner une augmentation du ramassage des ordures, ce qui peut affecter à son tour les performances de l'application.

  • Surcharges d’interaction de base de données : les applications qui exécutent un grand nombre de requêtes sur une base de données peuvent rencontrer des goulots d’étranglement liés aux interactions de base de données. Cela inclut des requêtes inefficaces, des appels de base de données excessifs et une mauvaise utilisation des fonctionnalités Entity Framework, qui peuvent toutes dégrader les performances.

L’étude de cas vise à résoudre ces problèmes en utilisant les outils de profilage de Visual Studio pour analyser les performances de l’application. En comprenant où et comment les performances de l’application peuvent être améliorées, les développeurs peuvent implémenter des optimisations pour réduire l’utilisation du processeur, améliorer l’efficacité de l’allocation de mémoire, rationaliser les interactions de base de données et optimiser l’utilisation des ressources. L’objectif ultime est d’améliorer les performances globales de l’application, ce qui rend l’exécution plus efficace et rentable.

Défi

La résolution des problèmes de performances dans l’exemple d’application .NET présente plusieurs défis. Ces défis découlent de la complexité du diagnostic des goulots d’étranglement des performances. Les principaux défis liés à la résolution des problèmes décrits sont les suivants :

  • Diagnostiquer les goulots d’étranglement des performances : l’un des principaux défis consiste à identifier avec précision les causes racines des problèmes de performance. Une utilisation élevée du processeur, une allocation de mémoire inefficace et des surcharges d’interaction de base de données peuvent avoir plusieurs facteurs de contribution. Les développeurs doivent utiliser efficacement des outils de profilage pour diagnostiquer ces problèmes, ce qui nécessite une compréhension de la façon dont ces outils fonctionnent et comment interpréter leur sortie.

  • contraintes de connaissances et de ressources: enfin, les équipes peuvent faire face à des contraintes liées aux connaissances, à l’expertise et aux ressources. Le profilage et l’optimisation d’une application nécessitent des compétences et une expérience spécifiques, et il se peut que toutes les équipes n’aient pas un accès immédiat à ces ressources.

La résolution de ces défis nécessite une approche stratégique qui combine une utilisation efficace des outils de profilage, des connaissances techniques et une planification et des tests minutieux. L’étude de cas vise à guider les développeurs dans ce processus, en fournissant des stratégies et des insights pour surmonter ces défis et améliorer les performances de l’application.

Stratégie

Voici une vue générale de l’approche dans cette étude de cas :

  • Nous commençons l’enquête en effectuant une trace d’utilisation du processeur. L’outil d’utilisation du processeur de Visual Studio est souvent utile pour commencer des enquêtes sur les performances et optimiser le code pour réduire les coûts.
  • Ensuite, pour obtenir des insights supplémentaires pour isoler les problèmes ou améliorer les performances, nous collectons une trace à l’aide de l’un des autres outils de profilage. Par exemple:
    • Nous examinons l’utilisation de la mémoire. Pour .NET, nous essayons d’abord l’outil d’allocation d’objets .NET. (Pour .NET ou C++, vous pouvez examiner l’outil Utilisation de la mémoire à la place.)
    • Pour ADO.NET ou Entity Framework, nous pouvons utiliser l’outil de base de données pour examiner les requêtes SQL, le temps de requête précis et bien plus encore.

La collecte de données nécessite les tâches suivantes :

  • Mettre l'application en mode compilation Release.
  • Sélection de l’outil Utilisation du processeur dans le Profileur de performances (Alt+F2). (Les étapes ultérieures impliquent quelques-uns des autres outils.)
  • À partir du Profileur de performances, démarrez l’application et collectez une trace.

Inspecter les zones d’utilisation élevée du processeur

Après avoir collecté une trace avec l’outil Utilisation du processeur et en la chargeant dans Visual Studio, nous vérifions d’abord la page de rapport initiale .diagsession qui affiche les données résumées. Utilisez le lien Ouvrir les détails dans le rapport.

Capture d’écran de l’ouverture des détails dans l’outil d'utilisation du processeur.

Dans la vue des détails du rapport, ouvrez la vue Arborescence des appels. Le chemin du code avec une utilisation maximale du processeur dans l’application est appelé chemin chaud. L’icône de flamme du chemin chaud (Capture d’écran montrant l’icône Chemin chaud.) peut aider à identifier rapidement les problèmes de performance susceptibles d’être améliorés.

Dans la vue Arborescence des appels, vous pouvez voir une utilisation élevée du processeur pour la méthode GetBlogTitleX dans l’application, représentant environ 60 % du partage de l'utilisation du processeur de l'application. Toutefois, la valeur du Processeur autonome pour GetBlogTitleX est faible, seulement environ .10 %. Contrairement à Total CPU, la valeur Self CPU exclut le temps passé dans d’autres fonctions, cela nous permet donc de regarder plus loin dans l’arborescence des appels pour le goulot d’étranglement réel.

Capture d’écran de l’arborescence des appels dans l’outil Utilisation du processeur.

GetBlogTitleX effectue des appels externes à deux DLL LINQ, qui utilisent la plupart du temps processeur, comme le montrent les valeurs très élevées du Processeur autonome. Il s’agit du premier indice indiquant qu’une requête LINQ peut être une zone à optimiser.

Capture d'écran de l’arborescence des appels dans l’outil Utilisation du processeur avec le processeur autonome en surbrillance.

Pour obtenir une arborescence d’appels visualisée et une vue différente des données, ouvrez la vue Graphique en flamme. (Ou, cliquez avec le bouton droit sur GetBlogTitleX et choisissez Affichage dans Flame Graph.) Ici encore, il semble que la méthode GetBlogTitleX soit responsable de l’utilisation du processeur de l’application (illustrée en jaune). Les appels externes aux DLL LINQ s’affichent sous la zone GetBlogTitleX, et ils utilisent l’ensemble du temps processeur pour la méthode.

Capture d’écran de la vue Graphique en flamme dans l’outil Utilisation du processeur.

Collecter des données supplémentaires

Souvent, d’autres outils peuvent fournir des informations supplémentaires pour aider l’analyse et isoler le problème. Dans cette étude de cas, nous prenons l’approche suivante :

  • Tout d’abord, examinez l’utilisation de la mémoire. Il peut y avoir une corrélation entre l’utilisation élevée du processeur et l’utilisation élevée de la mémoire. Il peut donc être utile d’examiner les deux pour isoler le problème.
  • Comme nous avons identifié les DLL LINQ, nous allons également examiner l’outil Base de données.

Vérifier l’utilisation de la mémoire

Pour voir ce qui se passe avec l’application en termes d’utilisation de la mémoire, nous collectons une trace à l’aide de l’outil d’allocation d’objets .NET (pour C++, vous pouvez utiliser l’outil Utilisation de la mémoire à la place). Vue Arborescence des appels dans la trace de mémoire montre le chemin critique et nous aide à identifier une zone d'utilisation élevée de la mémoire. Aucune surprise à ce stade, la méthode GetBlogTitleX semble générer beaucoup d’objets ! Plus de 900 000 allocations d’objets, en fait.

Capture d’écran de l’arborescence des appels dans l’outil d’allocation d’objets .NET.

La plupart des objets créés sont des chaînes, des tableaux d’objets et Int32s. Nous pouvons voir comment ces types sont générés en examinant le code source.

Vérifier la requête dans l’outil Base de données

Dans le Profileur de performances, nous sélectionnons l’outil base de données au lieu de l’utilisation du processeur (ou, sélectionnez les deux). Lorsque nous avons collecté une trace, ouvrez l’onglet Requêtes dans la page diagnostics. Dans l’onglet Requêtes de la trace de base de données, vous pouvez voir la première ligne affichant la requête la plus longue, 2446 ms. La colonne Enregistrements indique le nombre d’enregistrements lus par la requête. Vous pouvez utiliser ces informations pour une comparaison ultérieure.

Capture d’écran des requêtes de base de données dans l’outil Base de données.

En examinant l’instruction SELECT générée par LINQ dans la colonne Requête, nous identifions la première ligne comme requête associée à la méthode GetBlogTitleX. Pour afficher la chaîne de requête complète, développez la largeur de colonne. La chaîne de requête complète est la suivante :

SELECT "b"."Url", "b"."BlogId", "p"."PostId", "p"."Author", "p"."BlogId", "p"."Content", "p"."Date", "p"."MetaData", "p"."Title"
FROM "Blogs" AS "b" LEFT JOIN "Posts" AS "p" ON "b"."BlogId" = "p"."BlogId" ORDER BY "b"."BlogId"

Notez que l’application récupère beaucoup de valeurs de colonne ici, peut-être plus que nous n’en avons besoin. Examinons le code source.

Optimiser le code

Il est temps d’examiner le code source GetBlogTitleX. Dans l’outil Base de données, cliquez avec le bouton droit sur la requête et choisissez Accéder au fichier source. Dans le code source de GetBlogTitleX, nous trouvons le code suivant qui utilise LINQ pour lire la base de données.

foreach (var blog in db.Blogs.Select(b => new { b.Url, b.Posts }).ToList())
  {
    foreach (var post in blog.Posts)
    {
      if (post.Author == "Fred Smith")
      {
        Console.WriteLine($"Post: {post.Title}");
      }
  }
}

Ce code utilise foreach boucles pour rechercher dans la base de données les blogs avec « Fred Smith » comme auteur. Vous pouvez voir qu’un grand nombre d’objets sont générés en mémoire : un nouveau tableau d’objets pour chaque blog de la base de données, des chaînes associées pour chaque URL et des valeurs pour les propriétés contenues dans les billets, comme l’ID de blog.

Nous effectuons un peu de recherche et trouvons quelques recommandations courantes pour optimiser les requêtes LINQ. Nous pouvons également gagner du temps et laisser Copilot faire la recherche pour nous.

Si nous utilisons Copilot, nous sélectionnons Demander à Copilot dans le menu contextuel et tapez la question suivante :

Can you make the LINQ query in this method faster?

Conseil

Vous pouvez utiliser des commandes obliques telles que /optimize pour vous aider à poser de bonnes questions à Copilot.

Dans cet exemple, Copilot fournit les modifications de code suggérées suivantes, ainsi qu’une explication.

public void GetBlogTitleX()
{
    var posts = db.Posts
        .Where(post => post.Author == "Fred Smith")
        .Select(post => post.Title)
        .ToList();

    foreach (var postTitle in posts)
    {
        Console.WriteLine($"Post: {postTitle}");
    }
}

Ce code inclut plusieurs modifications pour optimiser la requête :

  • Ajout de la clause Where et élimination de l’une des boucles foreach.
  • Projeté uniquement la propriété Titre dans l’instruction Select, ce qui est tout ce dont nous avons besoin dans cet exemple.

Ensuite, nous retestons à l’aide des outils de profilage.

Résultats

Après avoir mis à jour le code, nous réexécutons l’outil Utilisation du processeur pour collecter une trace. La vue de l’arborescence des appels montre que GetBlogTitleX fonctionne pendant seulement 1754 ms, en utilisant 37% du total du processeur de l'application, une amélioration significative par rapport à 59%.

Capture d’écran de l’utilisation améliorée du processeur dans l’arborescence des appels de l’outil Utilisation du processeur.

Basculez vers la vue Flame Graph pour afficher une autre visualisation montrant l’amélioration. Dans cette vue, GetBlogTitleX utilise également une plus petite partie du processeur.

Capture d’écran de l’utilisation améliorée du processeur dans l’affichage Flame Graph de l’outil Utilisation du processeur.

Vérifiez les résultats dans la trace de l’outil de base de données et seuls deux enregistrements sont lus à l’aide de cette requête, au lieu de 100 000 ! En outre, la requête est beaucoup simplifiée et élimine la jointure LEFT inutile qui a été générée précédemment.

Capture d’écran du temps de requête plus rapide dans l’outil Base de données.

Ensuite, nous revérifions les résultats dans l’outil d’allocation d’objets .NET et nous voyons que GetBlogTitleX est uniquement responsable de 56 000 allocations d’objets, ce qui représente près d'une réduction de 95% par rapport à 900 000 !

Capture d’écran des allocations de mémoire réduites dans l’outil Allocation d’objets .NET.

Itérer

Plusieurs optimisations peuvent être nécessaires et nous pouvons continuer à itérer avec les modifications de code pour voir quelles modifications améliorent les performances et aident à réduire le coût de calcul.

Étapes suivantes

Les articles et billets de blog suivants fournissent plus d’informations pour vous aider à apprendre à utiliser efficacement les outils de performances de Visual Studio.