Partager via


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. Dans cette étude de cas, nous montrons comment vous pouvez utiliser les outils de profilage pour vous aider à accomplir cette tâche.

Notre objectif est d'apporter aux développeurs les connaissances nécessaires pour :

  • Comprendre l'importance de l'optimisation du code et son impact sur la réduction des coûts de calcul.
  • Utiliser les outils de profilage de Visual Studio pour analyser les performances des applications.
  • Interpréter les données fournies par ces outils pour identifier les goulots d'étranglement.
  • 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.

À la fin de ce guide, les lecteurs devraient être en mesure d'appliquer ces techniques à leurs propres projets, ce qui se traduira par des applications plus efficaces et plus rentables.

Étude de cas d'optimisation

L'exemple d'application présenté dans cette étude de cas est une application .NET conçue pour exécuter des requêtes sur une base de données de blogs et d'articles de blogs associés. 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 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 souffre d'un goulot d'étranglement commun 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 :

  • 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 étapes suivantes :

  • Nous configurons l'application en version Release.
  • Sélectionnez l'outil Utilisation du processeur dans le Profileur de performances (Alt+F2). (Les étapes ultérieures impliquent quelques-uns des autres outils.)
  • Dans le Performance Profiler, nous démarrons l'application et collectons 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, nous vérifions d'abord la page initiale du rapport .diagsession qui affiche Top Insights et Hot Path. Ce dernier indique le chemin de code qui utilise le plus de processeur dans l'application. Ces sections peuvent fournir des conseils pour nous aider à identifier rapidement les problèmes de performance que nous pouvons améliorer.

Nous pouvons également afficher le chemin chaud dans l'arborescence des appels. Pour ouvrir cet affichage, utilisez le lien Ouvrir les détails dans le rapport, puis sélectionnez Arborescence des appels.

Dans cette vue, nous voyons à nouveau le chemin chaud, qui montre une utilisation élevée du processeur pour la méthode GetBlogTitleX dans l'application, utilisant environ 60 % de l'utilisation du processeur de l'application. Toutefois, la valeur de l’auto-processeur pour GetBlogTitleX est faible, seulement environ .10%. Contrairement au processeur total, la valeur de l’auto-processeur exclut le temps passé dans d’autres fonctions. Nous savons qu’il faut regarder plus loin dans l’Arborescence des appels pour trouver 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 de l’auto-processeur. C'est le premier indice que nous pourrions chercher à optimiser une requête LINQ.

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

Pour visualiser l'arbre des appels et obtenir une vue différente des données, nous cliquons avec le bouton droit de la souris sur GetBlogTitleX et choisissons Vue dans le graphique Flame. Là encore, il semble que la méthode GetBlogTitleX soit responsable d’une grande partie de l’utilisation du processeur de l’application (illustré en jaune). Les appels externes aux DLL LINQ s’affichent sous la boîte GetBlogTitleX et ils utilisent tout le 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. Pour cet exemple, nous adoptons l’approche suivante :

  • Nous commençons par examiner 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 Performance Profiler, nous sélectionnons l'outil Database au lieu de CPU Usage (ou nous pouvons sélectionner les deux). Une fois la trace collectée, sélectionnez l'onglet Requêtes dans la page de diagnostic. Dans l'onglet Requêtes de la trace de la base de données, nous pouvons voir que la première ligne montre la requête la plus longue, 2446 ms. La colonne Enregistrements indique le nombre d’enregistrements que la requête lit. Nous pouvons 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 la chaîne de requête complète, nous élargissons 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 nous récupérons beaucoup de valeurs de colonnes 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, nous cliquons avec le bouton droit de la souris sur la requête et choisissons Aller 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. Nous constatons 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 articles, telles que l'ID du 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.

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.

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.

Nous passons à l'affichage du graphique Flame pour voir une autre visualisation 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.

Nous vérifions les résultats dans la trace de l'outil Base de données, et seuls deux enregistrements sont lus avec 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

De multiples 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 réduisent notre 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.