Conseils pour optimiser les programmes C # /. NET

Il semble que l’optimisation soit un art perdu de nos jours. N’y a-t-il pas eu un moment où tous les programmeurs ont tiré chaque once d’efficacité de leur code? Le fait souvent en marchant cinq milles dans la neige?

Dans le but de ramener un art perdu, quels sont les conseils que vous connaissez pour les changements simples (ou peut-être complexes) visant à optimiser le code C # / .NET? Comme il s’agit d’une chose si vaste qui dépend de ce que l’on essaie d’accomplir, cela aiderait à fournir un contexte avec votre conseil. Par exemple:

  • Lors de la concaténation de plusieurs chaînes, utilisez plutôt SsortingngBuilder . Voir le lien en bas pour les mises en garde à ce sujet.
  • Utilisez ssortingng.Compare pour comparer deux chaînes au lieu de faire quelque chose comme ssortingng1.ToLower() == ssortingng2.ToLower()

Le consensus général jusqu’ici semble mesurer est la clé. Ce genre de chose manque le point: mesurer ne vous dit pas ce qui ne va pas ou que faire si vous rencontrez un goulot d’étranglement. J’ai rencontré le goulot d’étranglement de la concaténation de chaînes une fois et je ne savais pas quoi faire à ce sujet, donc ces conseils sont utiles.

Mon point pour poster même ceci est d’avoir une place pour les goulots d’étranglement courants et comment ils peuvent être évités avant même de les rencontrer. Il ne s’agit même pas nécessairement du code plug-and-play que tout le monde devrait suivre aveuglément, mais plutôt de comprendre que la performance doit être envisagée, au moins quelque peu, et qu’il existe des pièges courants à rechercher.

Je peux voir cependant qu’il pourrait être utile de savoir pourquoi une astuce est utile et où elle devrait être appliquée. Pour le conseil SsortingngBuilder j’ai trouvé l’aide que j’ai faite il y a longtemps sur le site de Jon Skeet .

Il semble que l’optimisation soit un art perdu de nos jours.

Une fois par jour, la fabrication de microscopes, par exemple, était pratiquée comme un art. Les principes optiques étaient mal compris. Il n’y avait pas de standardisation des pièces. Les tubes, les engrenages et les lentilles devaient être fabriqués à la main par des travailleurs hautement qualifiés.

Ces microscopes sont aujourd’hui produits en tant que discipline d’ingénierie. Les principes sous-jacents de la physique sont extrêmement bien compris, les pièces disponibles dans le commerce sont largement disponibles et les ingénieurs en construction de microscopes peuvent faire des choix éclairés quant à la meilleure manière d’optimiser leur instrument pour les tâches à exécuter.

Cette parsing de performance est un “art perdu” est une très bonne chose. Cet art était pratiqué comme un art . L’optimisation doit être envisagée pour ce qu’elle est: un problème d’ingénierie pouvant être résolu par l’application minutieuse de principes techniques solides.

Au cours des années, on m’a demandé des dizaines de fois pour ma liste de “trucs et astuces” que les gens peuvent utiliser pour optimiser leur vbscript / leur jscript / leurs pages de serveur actives / leur code VB / C #. Je résiste toujours à cela. Mettre l’accent sur les «trucs et astuces» est exactement la mauvaise façon d’aborder les performances. De cette manière, le code est difficile à comprendre, difficile à raisonner, difficile à gérer, ce qui n’est généralement pas plus rapide que le code simple correspondant.

La bonne façon d’aborder la performance est de l’aborder comme un problème d’ingénierie comme tout autre problème:

  • Fixer des objectives significatifs, mesurables et axés sur le client.
  • Créez des suites de tests pour tester vos performances par rapport à ces objectives dans des conditions réalistes mais contrôlables et reproductibles.
  • Si ces suites montrent que vous n’atteignez pas vos objectives, utilisez des outils tels que les profileurs pour comprendre pourquoi.
  • Optimisez le contrôle sur ce que le profileur identifie comme le sous-système le moins performant. Gardez le profil de chaque modification afin de bien comprendre l’impact des performances de chacun.
  • Répétez jusqu’à ce que l’une des trois choses se produise (1) vous atteignez vos objectives et expédiez le logiciel, (2) vous révisez vos objectives à la baisse, ou (3) votre projet est annulé parce que vous ne pouvez pas atteindre vos objectives.

C’est comme si vous résolviez tout autre problème d’ingénierie, comme l’ajout d’une fonctionnalité – définissez des objectives centrés sur le client pour la fonctionnalité, suivez les progrès de l’implémentation solide, résolvez les problèmes en les analysant soigneusement, continuez d’itérer jusqu’à vous expédiez ou échouez. La performance est une fonctionnalité.

L’parsing de la performance sur des systèmes modernes complexes exige de la discipline et de se concentrer sur des principes techniques solides, et non sur un sac plein d’astuces qui sont étroitement applicables à des situations insignifiantes ou irréalistes. Je n’ai jamais résolu un problème de performance réel en appliquant des conseils et des astuces.

Obtenez un bon profileur.

N’essayez même pas d’optimiser C # (vraiment, n’importe quel code) sans un bon profileur. En fait, cela aide énormément à avoir à la fois un échantillonneur et un profileur de traçage.

Sans un bon profileur, vous risquez de créer de fausses optimisations et, surtout, d’optimiser les routines qui ne sont pas un problème de performances en premier lieu.

Les trois premières étapes du profilage doivent toujours être 1) Mesurer, 2) mesurer, puis 3) mesurer ….

Directives d’optimisation:

  1. Ne le faites pas à moins que vous ayez besoin de
  2. Ne le faites pas s’il est moins coûteux de lancer un nouveau matériel au lieu d’un développeur
  3. Ne le faites pas à moins de pouvoir mesurer les changements dans un environnement équivalent à la production
  4. Ne le faites pas à moins de savoir comment utiliser un processeur et un profileur de mémoire
  5. Ne le faites pas si cela va rendre votre code illisible ou impossible à maintenir

Comme les processeurs continuent à accélérer le processus, le principal problème dans la plupart des applications n’est pas le processeur, mais la bande passante: la bande passante à la mémoire hors puce, la bande passante sur le disque et la bande passante au réseau.

Commencez par l’extrémité distante: utilisez YSlow pour voir pourquoi votre site Web est lent pour les utilisateurs finaux, puis revenez en arrière et corrigez vos access de firebase database pour qu’ils ne soient pas trop larges (colonnes) et pas trop profonds (lignes).

Dans les très rares cas où il vaut la peine de faire quelque chose pour optimiser l’utilisation du processeur, veillez à ne pas avoir d’impact négatif sur la mémoire: j’ai vu des optimisations où les développeurs essayaient d’utiliser la mémoire pour mettre en cache les résultats. L’effet net a été de réduire la mémoire disponible pour mettre en cache les pages et les résultats de la firebase database, ce qui a rendu l’application beaucoup plus lente! (Voir la règle sur la mesure.)

J’ai également vu des cas où un algorithme «non stupide» et non optimisé a battu un algorithme optimisé «intelligent». Ne jamais sous-estimer à quel point les rédacteurs de compilateurs et les concepteurs de puces ont réussi à transformer le code de bouclage «inefficace» en un code extrêmement efficace pouvant fonctionner entièrement en mémoire sur puce avec traitement en pipeline. Votre algorithme «intelligent» basé sur une arborescence avec une boucle interne non compressée qui compte à rebours que vous pensiez «efficace» peut être battu simplement parce qu’il n’a pas réussi à restr dans la mémoire intégrée lors de l’exécution. (Voir la règle sur la mesure.)

Lorsque vous travaillez avec des ORM, tenez compte des sélections N + 1.

 List _orders = _repository.GetOrders(DateTime.Now); foreach(var order in _orders) { Print(order.Customer.Name); } 

Si les clients ne sont pas chargés avec impatience, cela peut entraîner plusieurs allers-retours vers la firebase database.

  • N’utilisez pas de nombres magiques, utilisez des énumérations
  • Ne pas coder des valeurs
  • Utilisez des génériques dans la mesure du possible, car il est compatible avec les types et évite la boxe et le déballage
  • Utilisez un gestionnaire d’erreur là où c’est absolument nécessaire
  • Jeter, éliminer, éliminer Le CLR ne sait pas comment fermer vos connexions de firebase database, fermez-les après utilisation et jetez les ressources non gérées
  • Utiliser le bon sens!

OK, je dois jeter dans mes favoris: Si la tâche est assez longue pour une interaction humaine, utilisez une coupure manuelle dans le débogueur.

Contre. un profileur, cela vous donne une stack d’appels et des valeurs variables que vous pouvez utiliser pour vraiment comprendre ce qui se passe.

Faites cela 10 à 20 fois et vous aurez une bonne idée de ce que l’optimisation pourrait vraiment faire.

Si vous identifiez une méthode comme un goulot d’étranglement, mais que vous ne savez pas quoi faire , vous êtes essentiellement bloqué.

Donc, je vais énumérer quelques choses. Toutes ces choses ne sont pas des balles d’argent et vous devrez toujours définir votre code. Je fais juste des suggestions pour des choses que vous pourriez faire et peut parfois aider. Surtout les trois premiers sont importants.

  • Essayez de résoudre le problème en utilisant uniquement (ou: principalement) des types ou des tableaux de bas niveau.
  • Les problèmes sont souvent mineurs – l’utilisation d’un algorithme intelligent mais complexe ne vous permet pas toujours de gagner, en particulier si l’algorithme moins intelligent peut être exprimé dans un code qui n’utilise que des tableaux de types bas niveau. Prenons par exemple InsertionSort vs MergeSort pour n <= 100 ou l'algorithme de recherche de Dominator de Tarjan ou l'utilisation de vecteurs pour résoudre naïvement la forme de flux de données du problème pour n <= 100. (le 100 est bien sûr juste pour vous donner une idée - profil !)
  • Pensez à écrire un cas particulier qui peut être résolu en utilisant uniquement des types de bas niveau (souvent des instances de taille <64), même si vous devez conserver l’autre code pour les problèmes plus importants.
  • Apprenez l’arithmétique binary pour vous aider avec les deux idées ci-dessus.
  • BitArray peut être votre ami, comparé à Dictionary, ou pire, List. Mais attention, l’implémentation n’est pas optimale. Vous pouvez écrire une version plus rapide vous-même. Au lieu de tester que vos arguments sont hors limites, etc., vous pouvez souvent structurer votre algorithme de sorte que l’index ne puisse pas sortir de toute façon – mais vous ne pouvez pas supprimer le contrôle de BitArray standard et ce n’est pas gratuit .
  • À titre d’exemple de ce que vous pouvez faire avec des tableaux de types bas niveau, BitMasortingx est une structure assez puissante qui peut être implémentée comme un tableau d’ulong et vous pouvez même la parcourir en utilisant un ulong comme “front” car vous pouvez prendre le bit de poids le plus bas dans le temps constant (par rapport à la queue dans Breadth First Search – mais évidemment, l’ordre est différent et dépend de l’ index des éléments plutôt que de l’ordre dans lequel vous les trouvez).
  • Division et modulo sont vraiment lents à moins que le côté droit ne soit une constante.
  • Les maths à virgule flottante ne sont généralement pas plus lents que les maths entiers (pas “quelque chose que vous pouvez faire”, mais “quelque chose que vous pouvez ignorer”)
  • La ramification n’est pas gratuite . Si vous pouvez l’éviter en utilisant une arithmétique simple (autre que division ou modulo), vous pouvez parfois obtenir des performances. Déplacer une twig à l’extérieur d’une boucle est presque toujours une bonne idée.

Les gens ont des idées amusantes sur ce qui compte réellement. Stack Overflow est plein de questions sur, par exemple, est ++i plus “performant” que i++ . Voici un exemple d’optimisation des performances , et c’est essentiellement la même procédure pour toutes les langues. Si le code est simplement écrit d’une certaine manière “parce que c’est plus rapide”, c’est une supposition.

Bien sûr, vous n’écrivez pas délibérément un code stupide, mais si vous deviniez que cela fonctionnait, les profileurs et les techniques de profilage seraient inutiles.

Dites au compilateur quoi faire, pas comment le faire. Par exemple, foreach (var item in list) est meilleur que for (int i = 0; i < list.Count; i++) et m = list.Max(i => i.value); vaut mieux que list.Sort(i => i.value); m = list[list.Count - 1]; list.Sort(i => i.value); m = list[list.Count - 1]; .

En indiquant au système ce que vous voulez faire, vous pouvez déterminer la meilleure façon de le faire. LINQ est bon parce que ses résultats ne sont pas calculés tant que vous n'en avez pas besoin. Si vous n'utilisez que le premier résultat, il n'est pas nécessaire de calculer le rest.

En fin de compte (et cela s'applique à toute la programmation), minimisez les boucles et minimisez ce que vous faites dans les boucles. Le plus important est de minimiser le nombre de boucles dans vos boucles. Quelle est la différence entre un algorithme O (n) et un algorithme O (n ^ 2)? L'algorithme O (n ^ 2) a une boucle à l'intérieur d'une boucle.

La vérité est qu’il n’existe pas de code optimisé parfait. Vous pouvez cependant optimiser pour une portion de code spécifique, sur un système (ou ensemble de systèmes) connu, sur un type de processeur (et un nombre) connu, une plate-forme connue (Microsoft? Mono ?), Une version framework / BCL connue, une version connue de la CLI, une version connue du compilateur (bogues, modifications de spécification, modifications), une quantité totale et disponible de mémoire connue, une origine d’assemblage connue (disque GAC ? distant?

Dans le monde réel, utilisez un profileur et examinez les bits importants; En général, les choses évidentes impliquent des E / S, tout ce qui implique le threading (encore une fois, cela change énormément entre les versions), et tout ce qui implique des boucles et des recherches, mais vous pourriez être surpris et quel code “évidemment bon” est un énorme coupable.

Je n’essaie pas vraiment d’optimiser mon code, mais parfois je vais utiliser quelque chose comme un réflecteur pour remettre mes programmes à la source. Il est intéressant de comparer ensuite ce que je ne comprends pas avec ce que le réflecteur produira. Parfois, je trouve que ce que j’ai fait sous une forme plus compliquée a été simplifié. Peut ne pas optimiser les choses mais m’aide à voir des solutions plus simples aux problèmes.