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 le temps de calcul signifie réduire les coûts, donc l’optimisation de votre code peut faire des économies. Cette étude de cas utilise un exemple d'application présentant des problèmes de performance pour montrer comment utiliser les outils de profilage afin d'améliorer l'efficacité. Si vous souhaitez comparer les outils de profilage, consultez la section Quel outil choisir ?

Cette étude de cas couvre les rubriques suivantes :

  • 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 de Visual Studio pour analyser les performances des applications.
  • Comment interpréter les données fournies par ces outils pour identifier les goulots d'étranglement en matière de performances.
  • Comment appliquer des stratégies pratiques pour optimiser le code, en se concentrant sur l'utilisation du processeur, l'allocation de la mémoire et les interactions avec la base de données.

Suivez la procédure et appliquez ensuite ces techniques à vos propres applications pour les rendre plus efficaces et plus rentables.

Étude de cas d'optimisation

L'exemple examiné dans cette étude de cas correspond à une application .NET qui exécute des requêtes sur une base de données de blogs et d'articles de blogs. Elle utilise Entity Framework, un ORM (Object-Relational Mapping) populaire pour .NET, pour interagir avec une base de données locale SQLite. L'application est structurée de manière à pouvoir exécuter un grand nombre de requêtes, simulant ainsi un scénario réel dans lequel une application .NET pourrait être amenée à traiter des tâches de recherche de données étendues. L'exemple d'application est une version modifiée de l'Exemple de démarrage d'Entity Framework.

Le principal problème de performance de l'application exemple réside dans la façon dont elle gère les ressources informatiques et interagit avec la base de données. L'application montre un goulot d'étranglement qui a un impact significatif sur son efficacité et, par conséquent, sur les coûts de calcul associés à son fonctionnement. Le problème présente les symptômes suivants :

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

  • Allocation de mémoire inefficace : Les applications peuvent parfois être confrontées à des problèmes liés à l'utilisation et à l'allocation de la mémoire. Dans les applications .NET, une gestion inefficace de la mémoire peut entraîner une augmentation de la collecte des déchets, ce qui peut affecter les performances de l'application.

  • Frais généraux d'interaction avec la base de données : Les applications qui exécutent un grand nombre de requêtes sur une base de données peuvent connaître des goulets d'étranglement liés aux interactions avec la base de données. Il s'agit notamment de requêtes inefficaces, d'appels excessifs à la base de données et d'une mauvaise utilisation des capacités de l'Entity Framework, autant de facteurs susceptibles de 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 mettre en œuvre des optimisations pour réduire l'utilisation du processeur, améliorer l'efficacité de l'allocation de la mémoire, rationaliser les interactions avec la base de données et optimiser l'utilisation des ressources. L'objectif final est d'améliorer les performances globales de l'application, ce qui la rend plus efficace et plus rentable.

Défi

La résolution des problèmes de performance dans l'exemple d'application .NET présente plusieurs défis. Ces défis découlent de la complexité du diagnostic des goulets d'étranglement des performances. Les principaux défis à relever pour résoudre les problèmes décrits sont les suivants :

  • Diagnostiquer les goulets d'étranglement des performances : L'un des principaux défis consiste à identifier avec précision les causes profondes des problèmes de performance. L'utilisation élevée du processeur, l'allocation inefficace de la mémoire et les frais généraux d'interaction avec la base de données peuvent avoir de multiples facteurs contributifs. Les développeurs doivent utiliser efficacement les outils de profilage pour diagnostiquer ces problèmes, ce qui nécessite une certaine compréhension du fonctionnement de ces outils et de l'interprétation de leurs résultats.

  • Contraintes liées aux connaissances et aux ressources : Enfin, les équipes peuvent être confrontées à des contraintes liées aux connaissances, à l'expertise et aux ressources. Le profilage et l'optimisation d'une application requièrent des compétences et une expérience spécifiques, et toutes les équipes n'ont pas forcément un accès immédiat à ces ressources.

Pour relever ces défis, il faut adopter une approche stratégique qui combine l'utilisation efficace des outils de profilage, les connaissances techniques, ainsi qu'une planification et des tests minutieux. Cette étude de cas vise à guider les développeurs tout au long de ce processus, en leur fournissant des stratégies et des aperçus pour surmonter ces défis et améliorer les performances de l'application.

Stratégie

Voici une vue d'ensemble de l'approche adoptée dans cette étude de cas :

  • Nous commençons l'enquête en prenant une trace de l'utilisation du processeur. L'outil d'utilisation du processeur de Visual Studio est souvent utile pour commencer les investigations sur les performances et pour optimiser le code afin de réduire les coûts.
  • Ensuite, pour obtenir des aperçus supplémentaires permettant d'isoler les problèmes ou d'améliorer les performances, nous recueillons 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 .NET Object Allocation. (Pour .NET ou C++, vous pouvez utiliser l'outil Memory Usage).
    • Pour ADO.NET ou Entity Framework, nous pouvons utiliser l'outil Base de données pour examiner les requêtes SQL, préciser le temps de requête, etc.

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

  • Paramétrer l'application sur une build de mise en production.
  • Sélectionner 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é un processeur avec l'outil Utilisation du processeur et l'avoir chargé dans Visual Studio, vérifiez tout d'abord la page de rapport .diagsession initiale qui affiche des 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 Utilisation du processeur.

Dans la vue détaillée du rapport, ouvrez la vue Arborescence des appels. Le chemin de code qui utilise le plus de processeur dans l'application est appelé chemin chaud. L'icône de flamme du chemin chaud (Capture d'écran montrant l'icône de chemin chaud.) peut aider à identifier rapidement les problèmes de performance susceptibles d'amélioration.

Dans la vue Arborescence des appels, vous pouvez constater l'utilisation élevée du processeur par la méthode GetBlogTitleX, qui représente environ 60 % de l'utilisation du processeur par l'application. Toutefois, la valeur de l’auto-processeur pour GetBlogTitleX est faible, seulement environ .10%. Contrairement à la valeur Total processeur, la valeur Processeur auto exclut le temps utilisé par d'autres fonctions, de sorte que nous savons qu'il faut chercher plus loin dans l'arborescence des appels pour trouver le véritable goulot d'étranglement.

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 de l’auto-processeur. C'est le premier indice qu'une requête LINQ pourrait être un domaine à optimiser.

Capture d’écran de l’arborescence des appels dans l’outil d’utilisation de l’UC avec l’UC en surbrillance.

Pour visualiser l'arborescence des appels et obtenir une vue différente des données, ouvrez le Graphique de type flamme. (Ou cliquez avec le bouton droit de la souris sur GetBlogTitleX et choisissez Affichage graphique de type flamme). Ici encore, il semble que la méthode GetBlogTitleX soit responsable d'une grande partie de l'utilisation du processeur par l'application (en jaune). Les appels externes aux DLL LINQ s’affichent sous la boîte GetBlogTitleX et utilisent la totalité du temps processeur pour la méthode.

Capture d’écran de l’affichage Graphique de type flamme dans l’outil Utilisation du processeur.

Collecter des données supplémentaires

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

  • Tout d'abord, examinez l'utilisation de la mémoire. Il peut y avoir une corrélation entre une utilisation élevée de l’UC et une 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 en utilisant l'outil d'allocation d'objets .NET (pour C++, vous pouvez utiliser l'outil d'utilisation de la mémoire à la place). La vue de l'arbre des appels dans la trace mémoire montre le chemin chaud et nous aide à identifier une zone où l'utilisation de la mémoire est élevée. Pas de 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 des Int32. Il est possible de 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, sélectionnez l'outil Base de données au lieu de l'outil Utilisation du processeur (ou bien sélectionnez les deux). Une fois la trace collectée, ouvrez l'onglet Requêtes dans la page de diagnostic. Dans l’onglet Requêtes de la trace de base de données, vous pouvez voir que la première ligne affiche la requête la plus longue, 2 446 ms. La colonne Enregistrements indique le nombre d’enregistrements que la requête lit. 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 Query, nous identifions la première ligne comme étant la requête associée à la méthode GetBlogTitleX. Pour afficher toute la chaîne de requête, élargissez la largeur de la 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"

Remarquez que l'application récupère beaucoup de valeurs de colonnes, peut-être plus que ce dont nous 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 des boucles foreach pour rechercher la base de données tous les blogs avec « Fred Smith » comme auteur. En l’examinant, vous pouvez voir que de nombreux 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 quelques recherches et trouvons des recommandations communes sur la manière d'optimiser les requêtes LINQ, ce qui nous amène à ce code.

Conseil

Alternativement, nous pouvons gagner du temps et laisser Copilot faire la recherche pour nous.

foreach (var x in db.Posts.Where(p => p.Author.Contains("Fred Smith")).Select(b => b.Title).ToList())
{
  Console.WriteLine("Post: " + x);
}

Dans ce code, nous avons apporté plusieurs modifications pour optimiser la requête :

  • Vous avez ajouté la clause Where et supprimé l’une des boucles foreach.
  • Projette uniquement la propriété Title dans l'instruction Select, ce qui est tout ce dont nous avons besoin dans cet exemple.

Ensuite, nous effectuons un nouveau test à l'aide des outils de profilage.

Optimiser le code avec Copilot

Si nous utilisons Copilot, nous pouvons demander à Copilot de rechercher des problèmes de performances pour nous. Sélectionnez 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 slash comme /optimiser pour aider à formuler de bonnes questions pour Copilot.

Dans cet exemple, Copilot propose les modifications de code suggérées suivantes, similaires à notre requête optimisée, 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}");
    }
}

Résultats

Après avoir mis à jour le code, nous exécutons à nouveau l'outil Utilisation du processeur pour collecter une trace. L’affichage Arborescence des appels montre que GetBlogTitleX ne s’exécute que 1 754 ms, en utilisant 37 % du processeur total 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.

Passez à l'affichage Graphique de type flamme pour obtenir une vue différente de l'amélioration. Dans cet affichage, GetBlogTitleX utilise également une plus petite partie du processeur.

Capture d’écran de l’utilisation améliorée du processeur dans l’affichage Graphique de type flamme 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 de temps de requête plus rapide dans l’outil Base de données.

Ensuite, nous vérifions à nouveau les résultats dans l'outil .NET Object Allocation, et nous constatons que GetBlogTitleX n'est responsable que de 56 000 allocations d'objets, soit une réduction de près de 95 % par rapport à 900 000!

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

Itérer

Plusieurs optimisations peuvent être nécessaires et nous pouvons continuer à itérer avec des changements de code pour voir quels changements 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 Visual Studio.