Je travaille dans un magasin .Net, C # et j'ai un collègue qui insiste pour que nous utilisions des instructions Switch géantes dans notre code avec beaucoup de "Cas" plutôt que des approches plus orientées objet. Son argument remonte constamment au fait qu'une instruction Switch est compilée dans une "table de saut de processeur" et constitue donc l'option la plus rapide (même si, dans d'autres cas, on dit à notre équipe que nous ne nous soucions pas de la vitesse).
Honnêtement, je n'ai rien contre ça ... parce que je ne sais pas de quoi il parle.
A-t-il raison?
Est-ce qu'il parle juste de son cul?
J'essaie juste d'apprendre ici.
c#
.net
switch-statement
James P. Wright
la source
la source
Réponses:
Il est probablement un vieux hacker et oui, il parle hors de ses fesses. .Net n'est pas C ++; le compilateur .Net continue de s'améliorer et la plupart des astuces intelligentes sont contre-productives, si ce n'est aujourd'hui dans la prochaine version .Net. Les petites fonctions sont préférables car .Net JIT-s chaque fonction une fois avant son utilisation. Donc, si certains cas ne sont jamais touchés pendant le cycle de vie d'un programme, aucun coût n'est encouru pour les compiler par JIT. Quoi qu'il en soit, si la vitesse n'est pas un problème, il ne devrait pas y avoir d'optimisation. Écrivez pour le programmeur en premier, pour le compilateur en second. Votre collègue ne sera pas facilement convaincu. Je prouverais donc empiriquement qu'un code mieux organisé est en réalité plus rapide. Je choisirais l'un de ses pires exemples, les récrirais mieux, puis je m'assurerais que votre code est plus rapide. Cherry-pick si vous devez. Puis lancez-le quelques millions de fois, profilez-le et montrez-le-lui.
MODIFIER
Bill Wagner a écrit:
Point 11: Comprendre l’attrait des petites fonctions (Effective C # Deuxième édition) Rappelez-vous que la traduction de votre code C # en code exécutable par la machine est un processus en deux étapes. Le compilateur C # génère IL qui est livré dans les assemblys. Le compilateur JIT génère un code machine pour chaque méthode (ou groupe de méthodes, lorsque l’inlining est impliqué), selon les besoins. De petites fonctions permettent au compilateur JIT d'amortir ce coût plus facilement. Les petites fonctions sont également plus susceptibles d'être candidates à l'inline. Ce n'est pas juste la petitesse: un flux de contrôle plus simple est tout aussi important. Moins de branches de contrôle à l’intérieur des fonctions facilitent l’enregistrement de variables par le compilateur JIT. Ce n'est pas seulement une bonne pratique d'écrire du code plus clair; c'est comment vous créez un code plus efficace au moment de l'exécution.
EDIT2:
Donc ... apparemment, une instruction switch est plus rapide et meilleure qu'une série d'instructions if / else, car une comparaison est logarithmique et une autre linéaire. http://sequence-points.blogspot.com/2007/10/why-is-sswitch-statement-faster-than-if.html
Eh bien, mon approche préférée pour remplacer une énorme instruction switch consiste à utiliser un dictionnaire (ou parfois même un tableau si je commute sur des énums ou des petits ints) mappant des valeurs sur des fonctions appelées en réponse. Cela oblige à supprimer beaucoup de spaghettis partagés, mais c’est une bonne chose. Une déclaration de commutateur de grande taille est généralement un cauchemar de maintenance. Donc ... avec les tableaux et les dictionnaires, la recherche prendra un temps constant, et il y aura peu de mémoire supplémentaire perdue.
Je ne suis toujours pas convaincu que la déclaration de commutateur est meilleure.
la source
À moins que votre collègue ne puisse prouver que cette modification offre un avantage réel et mesurable à l'échelle de l'application dans son ensemble, elle est inférieure à votre approche (par exemple, le polymorphisme), qui offre en réalité un tel avantage: la maintenabilité.
La microoptimisation ne doit être effectuée qu'après que les goulots d'étranglement ont été identifiés . L'optimisation prématurée est la racine de tout mal .
La vitesse est quantifiable. Il y a peu d'informations utiles dans "l'approche A est plus rapide que l'approche B". La question est " combien plus vite? ".
la source
Qui se soucie si c'est plus rapide?
Sauf si vous écrivez un logiciel en temps réel, il est peu probable qu'une accélération minime que vous pourriez éventuellement obtenir en faisant quelque chose d'une manière complètement folle fera beaucoup de différence pour votre client. Je ne voudrais même pas me battre contre celui-ci sur le front de la vitesse, ce gars ne va clairement pas écouter aucun argument sur le sujet.
La facilité de maintenance, cependant, est l’objectif du jeu, et une déclaration de commutateur géant n’est même pas légèrement maintenable. Comment expliquez-vous les différents chemins empruntés par le code pour les nouveaux gars? La documentation devra être aussi longue que le code lui-même!
De plus, vous êtes alors totalement incapable de réaliser des tests unitaires (trop de chemins possibles, sans parler du manque probable d'interfaces, etc.), ce qui rend votre code encore moins maintenable.
[Du côté de l'être intéressé: JITter fonctionne mieux avec des méthodes plus petites, donc les instructions de commutateur géant (et leurs méthodes intrinsèquement volumineuses) vont nuire à votre vitesse dans les grands assemblages, IIRC.]
la source
Éloignez-vous de l'instruction switch ...
Ce type d’instruction doit être évité comme un fléau, car il enfreint le principe de fermeture . Cela oblige l'équipe à modifier le code existant lorsqu'une nouvelle fonctionnalité doit être ajoutée, par opposition à l'ajout d'un nouveau code.
la source
J'ai survécu au cauchemar connu sous le nom d'énorme machine à états finis manipulée par d'énormes déclarations de commutateur. Pire encore, dans mon cas, le FSM couvrait trois DLL C ++ et il était évident que le code avait été écrit par une personne connaissant le C.
Les métriques dont vous devez vous soucier sont:
J'avais pour tâche d'ajouter une nouvelle fonctionnalité à cet ensemble de DLL et de convaincre la direction qu'il me faudrait autant de temps pour réécrire les 3 DLL sous la forme d'une seule DLL proprement orientée objet que ce serait mon cas et jury rigider la solution dans ce qui était déjà là. La réécriture a été un franc succès, car elle supportait non seulement la nouvelle fonctionnalité mais était beaucoup plus facile à étendre. En fait, une tâche qui prendrait normalement une semaine pour vous assurer de ne rien casser prendrait finalement quelques heures.
Alors, qu'en est-il des temps d'exécution? Il n'y avait aucune augmentation ou diminution de la vitesse. Pour être honnête, notre performance a été réduite par les pilotes système. Si la solution orientée objet était en fait plus lente, nous ne le saurions pas.
Quel est le problème avec les déclarations de commutateur massives pour un langage OO?
la source
Je n'achète pas l'argument de performance; tout est question de maintenabilité du code.
MAIS: parfois , une instruction de commutateur géant est plus facile à gérer (moins de code) qu'un groupe de petites classes remplaçant les fonctions virtuelles d'une classe de base abstraite. Par exemple, si vous implémentiez un émulateur de CPU, vous ne mettriez pas en œuvre les fonctionnalités de chaque instruction dans une classe distincte. Vous l'inséreriez simplement dans un commutateur géant sur l'opcode, appelant éventuellement des fonctions d'assistance pour des instructions plus complexes.
Règle de base: si le changement est effectué d'une manière ou d'une autre sur le TYPE, vous devriez probablement utiliser l'héritage et les fonctions virtuelles. Si le changement est effectué sur une valeur de type fixe (par exemple, le code d'opération d'instruction, comme ci-dessus), vous pouvez le laisser tel quel.
la source
Vous ne pouvez pas me convaincre que:
Est significativement plus rapide que:
De plus, la version OO est simplement plus maintenable.
la source
Il a raison de dire que le code machine résultant sera probablement plus efficace. L'essentiel du compilateur transforme une instruction switch en un ensemble de tests et de branches, qui seront relativement peu d'instructions. Il y a de fortes chances que le code résultant d'approches plus abstraites nécessitera plus d'instructions.
CEPENDANT : il est presque certainement vrai que votre application particulière n'a pas besoin de s'inquiéter de ce type de micro-optimisation, sinon vous n'utiliseriez pas .net au départ. Pour tout ce qui concerne des applications intégrées très limitées ou un travail exigeant en ressources CPU, laissez toujours le compilateur se charger de l'optimisation. Concentrez-vous sur l'écriture d'un code propre et maintenable. Cela vaut presque toujours beaucoup plus que quelques dixièmes de nanosecondes en temps d'exécution.
la source
L'une des principales raisons d'utiliser des classes au lieu d'instructions switch est que ces dernières ont tendance à conduire à un fichier très volumineux comportant beaucoup de logique. C’est à la fois un cauchemar de maintenance et un problème de gestion des sources car vous devez extraire et éditer cet énorme fichier au lieu d’un autre fichier de classe plus petit.
la source
une instruction switch dans le code OOP est une forte indication des classes manquantes
essayez-le dans les deux sens et effectuez des tests de vitesse simples; les chances sont la différence ne sont pas significatives. Si tel est le cas et que le code est critique en termes de temps, conservez l'instruction switch
la source
Normalement, je déteste le mot "optimisation prématurée", mais ça en a une odeur. Il est intéressant de noter que Knuth a utilisé cette citation célèbre dans le cadre d'une incitation à utiliser des
goto
instructions afin d'accélérer le code dans des domaines critiques . C'est la clé: les chemins critiques .Il proposait d'utiliser
goto
pour accélérer le code, mais en mettant en garde les programmeurs qui voudraient faire ce genre de choses basées sur des intuitions et des superstitions pour un code qui n'est même pas critique.Favoriser les
switch
déclarations autant que possible uniformément dans une base de code (qu’une charge lourde soit gérée ou non) est l’exemple classique de ce que Knuth appelle le programmeur "optimiste" qui passe toute la journée à lutter pour maintenir leur "optimisation". "Code qui s’est transformé en cauchemar de débogage en essayant de sauver des sous au lieu de trois livres. Un tel code est rarement maintenable et encore moins efficace en premier lieu.Il a raison du point de vue fondamental de l'efficacité. À ma connaissance, aucun compilateur ne peut optimiser un code polymorphe impliquant des objets et une répartition dynamique mieux qu'une instruction switch. Vous ne vous retrouverez jamais avec une table de conversion ou une table de saut dans le code incorporé du code polymorphe, car ce code a tendance à servir de barrière d'optimisation au compilateur (il ne saura pas quelle fonction appeler jusqu'à l'heure à laquelle la répartition dynamique se produit).
Il est plus utile de ne pas penser à ce coût en termes de tables de saut, mais plutôt en termes d'obstacle d'optimisation. Pour le polymorphisme, l'appel
Base.method()
n'autorise pas le compilateur à savoir quelle fonction sera appelée si ellemethod
est virtuelle, non scellée et peut être remplacée. Comme il ne sait pas quelle fonction va être appelée à l'avance, il ne peut pas optimiser l'appel de fonction et utiliser plus d'informations pour prendre des décisions d'optimisation, car il ne sait pas en fait à quelle fonction il sera appelé. l'heure à laquelle le code est compilé.Les optimiseurs sont à leur meilleur lorsqu'ils peuvent scruter un appel de fonction et effectuer des optimisations qui aplatissent complètement l'appelant et l'appelé, ou au moins optimisent l'appelant pour travailler plus efficacement avec l'appelé. Ils ne peuvent pas faire cela s'ils ne savent pas quelle fonction sera appelée à l'avance.
Utiliser ce coût, qui équivaut souvent à des sous, pour justifier de le transformer en une norme de codage appliquée uniformément est généralement très ridicule, en particulier pour les lieux qui ont besoin d’extensibilité. C’est la principale chose à surveiller avec les véritables optimiseurs prématurés: ils veulent transformer les problèmes de performances mineurs en normes de codage appliquées uniformément dans une base de code sans se soucier de la maintenabilité.
Je m'offusque cependant un peu de la citation "old C hacker" utilisée dans la réponse acceptée, puisque je suis l'un de ceux-là. Toutes les personnes qui codent depuis des décennies à partir de matériel très limité ne sont pas devenues un optimiseur prématuré. Pourtant, j'ai rencontré et travaillé avec eux aussi. Mais ces types ne mesurent jamais des erreurs telles que des erreurs de prédiction de branche ou des erreurs de cache, ils pensent en savoir plus et fondent leurs notions d'inefficacité sur une base de code de production complexe basée sur des superstitions qui ne sont pas vraies aujourd'hui et qui parfois ne le sont jamais. Les personnes qui ont véritablement travaillé dans des domaines critiques en matière de performance comprennent souvent que l'optimisation efficace est une priorisation efficace. Tenter de généraliser une norme de codage dégradant la maintenabilité afin d'économiser des sous est une hiérarchisation très inefficace.
Les sous sont importants lorsque vous avez une fonction peu coûteuse qui ne fait pas autant de travail, appelée un milliard de fois dans une boucle très étroite et critique en termes de performances. Dans ce cas, nous finissons par économiser 10 millions de dollars. Il ne vaut pas la peine de se couper des sous lorsque vous avez une fonction appelée deux fois pour laquelle le corps coûte à lui seul des milliers de dollars. Il n’est pas sage de passer son temps à marchander des sous lors de l’achat d’une voiture. Il vaut la peine de marchander des sous si vous achetez un million de canettes de soda à un fabricant. La clé d'une optimisation efficace consiste à comprendre ces coûts dans leur contexte approprié. Quelqu'un qui essaie d'économiser des sous sur chaque achat et suggère que tous les autres essayent de négocier des sous, peu importe ce qu'ils achètent, n'est pas un optimiseur qualifié.
la source
On dirait que votre collègue est très préoccupé par les performances. Il se peut que dans certains cas, une structure cas / commutateur volumineuse fonctionne plus rapidement, mais nous espérons que vous feriez une expérience en effectuant des tests de synchronisation sur la version OO et la version commutateur / cas. Je suppose que la version OO a moins de code et est plus facile à suivre, à comprendre et à maintenir. Je dirais tout d’abord pour la version OO (car la maintenance / la lisibilité devrait être plus importante au départ), et n’envisagez la version switch / case que si la version OO pose de sérieux problèmes de performances et que l’on peut démontrer qu’un switch / case fera amélioration significative.
la source
L'un des avantages du polymorphisme en termes de facilité de maintenance, que personne n'a encore mentionné, est que vous pourrez structurer votre code de manière beaucoup plus efficace en utilisant l'héritage si vous basculez toujours sur la même liste de cas, mais parfois plusieurs cas sont traités de la même manière et parfois ne sont pas
Par exemple. si vous basculez entre
Dog
,Cat
etElephant
et parfoisDog
et que vousCat
avez le même cas, vous pouvez les faire hériter d'une classe abstraiteDomesticAnimal
et placer ces fonctions dans la classe abstraite.De plus, j'ai été surpris de constater que plusieurs personnes ont utilisé un analyseur syntaxique comme exemple d'utilisation du polymorphisme. Pour un analyseur ressemblant à une arborescence, c'est certainement la mauvaise approche, mais si vous avez quelque chose comme Assemblage, où chaque ligne est quelque peu indépendante et commencez avec un opcode qui indique comment le reste de la ligne doit être interprété, j'utiliserais totalement le polymorphisme. et une usine. Chaque classe peut implémenter des fonctions comme
ExtractConstants
ouExtractSymbols
. J'ai utilisé cette approche pour un interprète BASIC jouet.la source
"Nous devrions oublier les petites économies, disons environ 97% du temps: une optimisation prématurée est la racine de tout mal"
Donald Knuth
la source
Même si ce n'était pas mauvais pour la maintenabilité, je ne crois pas que ce sera meilleur pour la performance. Un appel de fonction virtuelle est simplement une indirection supplémentaire (identique au meilleur des cas pour une instruction switch). Ainsi, même en C ++, les performances doivent être sensiblement égales. En C #, où tous les appels de fonction sont virtuels, l’instruction switch devrait être encore pire, car vous avez le même temps système d’appel de fonction virtuelle dans les deux versions.
la source
Votre collègue ne parle pas de derrière, en ce qui concerne le commentaire sur les tables de saut. Cependant, utiliser cela pour justifier l'écriture de mauvais code est l'endroit où il se trompe.
Le compilateur C # convertit les instructions switch avec quelques cas en une série de if / else, il n’est donc pas plus rapide que d’utiliser if / else. Le compilateur convertit les instructions switch plus grandes en dictionnaire (la table de saut à laquelle votre collègue fait référence). Veuillez consulter cette réponse à une question sur le dépassement de capacité sur le sujet pour plus de détails .
Une instruction switch de grande taille est difficile à lire et à maintenir. Un dictionnaire de "cas" et de fonctions est beaucoup plus facile à lire. Comme c’est ce que l’on change en commutant, vous et votre collègue seriez bien avisé d’utiliser les dictionnaires directement.
la source
Il ne parle pas nécessairement de ses fesses. Au moins en C et C ++, les
switch
instructions peuvent être optimisées pour sauter des tables alors que cela ne s'est jamais produit avec une répartition dynamique dans une fonction qui n'a accès qu'à un pointeur de base. À tout le moins, ce dernier nécessite un optimiseur beaucoup plus intelligent qui examine un code beaucoup plus environnant pour déterminer exactement quel sous-type est utilisé à partir d'un appel de fonction virtuel via un pointeur / une référence de base.En plus de cela, la répartition dynamique sert souvent de "barrière d'optimisation", ce qui signifie que le compilateur ne sera souvent pas en mesure de code en ligne et d'allouer de manière optimale les registres afin de minimiser les débordements de pile et autres éléments fantaisistes, car il ne sait pas quoi La fonction virtuelle va être appelée via le pointeur de base pour l’aligner et faire toute sa magie d’optimisation. Je ne suis même pas sûr que vous souhaitiez même que l'optimiseur soit aussi intelligent et que vous tentiez d'optimiser les appels de fonction indirects, car cela pourrait potentiellement générer la génération de nombreuses branches de code séparément dans une pile d'appels donnée (une fonction que les appels
foo->f()
auraient générer un code machine totalement différent de celui qui appellebar->f()
via un pointeur de base, et la fonction qui appelle cette fonction doit alors générer deux versions de code ou plus, et ainsi de suite - la quantité de code machine générée serait explosive - peut-être pas si mal avec une trace JIT qui génère le code à la volée lorsqu’il trace à travers des chemins d’exécution chaud).Cependant, comme de nombreuses réponses l’ont fait écho, c’est une mauvaise raison de privilégier un grand nombre de
switch
déclarations, même si elles sont rapidement réduites à la vitesse de la main. En outre, en ce qui concerne les micro-efficacités, les priorités telles que la création de branches et l’inscription en ligne ne sont généralement pas prioritaires, par rapport aux configurations telles que les modèles d’accès à la mémoire.Cela dit, j'ai sauté ici avec une réponse inhabituelle. Je souhaite plaider en faveur de la maintenabilité des
switch
déclarations par rapport à une solution polymorphe lorsque, et uniquement si, vous êtes certain de savoir qu’il n’y aura qu’un seul endroit pour effectuer leswitch
.Un exemple typique est un gestionnaire d’événements central. Dans ce cas, vous n’avez généralement pas beaucoup d’endroits qui gèrent des événements, un seul (pourquoi c’est «central»). Dans ces cas, vous ne bénéficiez pas de l'extensibilité fournie par une solution polymorphe. Une solution polymorphe est bénéfique lorsque de nombreux endroits effectuent la
switch
déclaration analogique . Si vous savez avec certitude qu'il n'y en aura qu'un seul, uneswitch
instruction avec 15 observations peut être beaucoup plus simple que de concevoir une classe de base héritée de 15 sous-types avec des fonctions surchargées et une fabrique pour les instancier, pour ensuite les utiliser dans une fonction. dans l'ensemble du système. Dans ces cas, l'ajout d'un nouveau sous-type est beaucoup plus fastidieux que l'ajout d'unecase
instruction à une fonction. Si quelque chose, je plaiderais pour la maintenabilité, pas la performance,switch
déclarations dans ce cas particulier où vous ne bénéficiez d'aucune extensibilité.la source