Pourquoi utiliser une approche orientée objet au lieu d'un énoncé de «commutateur» géant?

59

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.

James P. Wright
la source
7
Vous pouvez vérifier s'il a raison en utilisant quelque chose comme .NET Reflector pour examiner le code d'assemblage et rechercher la "table de saut de l'unité centrale".
FrustratedWithFormsDesigner
5
"L'instruction de commutateur est compilée dans une" table de saut de processeur "Ainsi, la méthode la plus défavorable est la distribution avec toutes les fonctions virtuelles pures. Aucune fonction virtuelle n'est simplement liée directement. Avez-vous vidé du code pour comparer?
S.Lott
64
Le code doit être écrit pour PERSONNES pas pour les machines, sinon nous ferions tout en assembleur.
maple_shaft
8
S'il est un peu comme une nouille, citez Knuth: "Nous devrions oublier les petites économies, disons environ 97% du temps: l'optimisation prématurée est la racine de tous les maux."
DaveE
12
Maintenabilité. Avez-vous d'autres questions avec des réponses en un mot pour lesquelles je peux vous aider?
Matt Ellen

Réponses:

48

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.

Emploi
la source
47
Ne vous inquiétez pas pour le prouver plus rapidement. Ceci est une optimisation prématurée. La milliseconde que vous pourriez économiser n’est rien comparée à l’index que vous avez oublié d’ajouter à la base de données et qui vous coûte 200 ms. Vous faites la mauvaise bataille.
Rein Henrichs
27
@Job quoi s'il a réellement raison? Le fait n'est pas qu'il a tort, mais bien qu'il a raison et que cela n'a pas d'importance .
Rein Henrichs
2
Même s'il avait raison dans environ 100% des cas, il perd encore notre temps.
Jeremy
6
Je veux m'arracher les yeux en essayant de lire la page que vous avez liée.
AttaquerHobo
3
C'est quoi la haine C ++? Les compilateurs C ++ s'améliorent également et les gros commutateurs sont aussi mauvais en C ++ qu'en C #, et pour la même raison. Si vous êtes entouré d'anciens programmeurs C ++ qui vous chagrinent, ce n'est pas parce qu'ils sont programmeurs C ++, c'est parce qu'ils sont de mauvais programmeurs.
Sebastian Redl
39

À 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? ".

back2dos
la source
2
Absolument vrai. Ne jamais prétendre que quelque chose est plus rapide, toujours mesurer. Et ne mesurez que lorsque cette partie de l'application constitue le goulot d'étranglement des performances.
Kilian Foth
6
-1 pour "l'optimisation prématurée est la racine de tout le mal." S'il vous plaît afficher la citation entière , pas seulement une partie qui biaise l'opinion de Knuth.
alternative
2
@mathepic: Je n'ai intentionnellement pas présenté cela comme une citation. Cette phrase, telle quelle, est mon opinion personnelle, bien que ce ne soit évidemment pas ma création. Bien que l'on puisse noter que les gars de c2 semblent considérer uniquement cette partie comme la sagesse fondamentale.
back2dos
8
@alternative La citation complète de Knuth "Il ne fait aucun doute que le graal de l'efficacité conduit à des abus. Les programmeurs perdent énormément de temps à penser à la vitesse des parties non critiques de leurs programmes, et ces tentatives d'efficacité ont en réalité Un impact négatif important lorsque le débogage et la maintenance sont pris en compte. Nous devons oublier les petites efficacités, disons environ 97% du temps: une optimisation prématurée est la racine de tout mal. " Décrit parfaitement le collègue du PO. IMHO back2dos a bien résumé la citation: "L'optimisation prématurée est la racine de tout mal"
MarkJ
2
@MarkJ 97% du temps
alternative
27

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.]

Ed James
la source
1
+ un énorme exemple d'optimisation prématurée.
ShaneC
Certainement ça.
DeadMG
+1 pour 'une déclaration de commutateur géant n'est même pas légèrement maintenable'
Korey Hinton
2
Une déclaration de commutateur géant est beaucoup plus facile à comprendre pour le nouveau type: tous les comportements possibles sont rassemblés ici dans une belle liste ordonnée. Les appels indirects sont extrêmement difficiles à suivre. Dans le pire des cas (pointeur de fonction), vous devez rechercher dans la base de code entière les fonctions de la bonne signature, et les appels virtuels ne sont qu'un peu mieux (recherche des fonctions du bon nom et de la bonne liés par héritage). Mais la maintenabilité ne consiste pas à être en lecture seule.
Ben Voigt
14

É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.

Dakotah North
la source
11
Cela vient avec une mise en garde. Il existe des opérations (fonctions / méthodes) et des types. Lorsque vous ajoutez une nouvelle opération, vous ne devez modifier le code qu’à un seul endroit pour les instructions switch (ajoutez une nouvelle fonction avec une instruction switch), mais vous devez ajouter cette méthode à toutes les classes du cas OO (violation / principe fermé). Si vous ajoutez de nouveaux types, vous devez toucher chaque instruction switch, mais dans le cas OO, vous n'aurez plus qu'à ajouter une classe supplémentaire. Par conséquent, pour prendre une décision éclairée, vous devez savoir si vous allez ajouter d'autres opérations aux types existants ou en ajouter d'autres.
Scott Whitlock
3
Si vous devez ajouter plus d'opérations aux types existants dans un paradigme OO sans violer OCP, alors je pense que c'est à cela que le modèle de visiteur s'adresse.
Scott Whitlock
3
@Martin - nommez-moi si vous voulez, mais c'est un compromis bien connu. Je vous renvoie à Clean Code par RC Martin. Il revisite son article sur OCP en expliquant ce que j'ai décrit ci-dessus. Vous ne pouvez pas concevoir simultanément pour toutes les exigences futures. Vous devez choisir entre l'opportunité d'ajouter plus d'opérations ou plusieurs types. OO favorise l'ajout de types. Vous pouvez utiliser OO pour ajouter d'autres opérations si vous modélisez les opérations sous forme de classes, mais cela semble entrer dans le modèle de visiteur, qui a ses propres problèmes (notamment la surcharge).
Scott Whitlock
8
@ Martin: Avez-vous déjà écrit un analyseur? Il est assez courant d'avoir de grands cas de commutation qui activent le prochain jeton dans la mémoire tampon d'anticipation. Vous pouvez remplacer ces commutateurs par des appels de fonction virtuels au prochain jeton, mais ce serait un cauchemar de maintenance. C'est rare, mais parfois le commutateur est en fait le meilleur choix, car il conserve le code qui doit être lu / modifié ensemble à proximité.
Nikie
1
@Martin: Vous avez utilisé des mots tels que "jamais", "jamais" et "Coquelicot", alors je supposais que vous parliez de tous les cas sans exception, pas seulement du cas le plus courant. (Et BTW: Les gens écrivent encore des analyseurs syntaxiques à la main. Par exemple, l'analyseur CPython est toujours écrit à la main, IIRC.)
Nikie
8

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:

  • Vitesse de changement
  • Vitesse de trouver le problème quand il se produit

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?

  • Le flux de contrôle du programme est retiré de l'objet auquel il appartient et placé à l'extérieur de l'objet
  • De nombreux points de contrôle externe se traduisent par de nombreux endroits à revoir.
  • Il est difficile de savoir où l’état est stocké, en particulier si le commutateur est à l’intérieur d’une boucle
  • La comparaison la plus rapide est sans comparaison (vous pouvez éviter de nombreuses comparaisons avec une conception orientée objet de bonne qualité)
  • Il est plus efficace de parcourir vos objets et d'appeler toujours la même méthode sur tous les objets que de modifier votre code en fonction du type d'objet ou de l'énumération qui code le type.
Berin Loritsch
la source
8

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.

zvrba
la source
5

Vous ne pouvez pas me convaincre que:

void action1()
{}

void action2()
{}

void action3()
{}

void action4()
{}

void doAction(int action)
{
    switch(action)
    {
        case 1: action1();break;
        case 2: action2();break;
        case 3: action3();break;
        case 4: action4();break;
    }
}

Est significativement plus rapide que:

struct IAction
{
    virtual ~IAction() {}
    virtual void action() = 0;
}

struct Action1: public IAction
{
    virtual void action()    { }
}

struct Action2: public IAction
{
    virtual void action()    { }
}

struct Action3: public IAction
{
    virtual void action()    { }
}

struct Action4: public IAction
{
    virtual void action()    { }
}

void doAction(IAction& actionObject)
{
    actionObject.action();
}

De plus, la version OO est simplement plus maintenable.

Martin York
la source
8
Pour certaines choses et pour de plus petites quantités d’actions, la version OO est beaucoup plus rigoureuse. Il doit avoir une sorte d’usine pour convertir une valeur en création d’une action. Dans de nombreux cas, il est beaucoup plus lisible de simplement activer cette valeur.
Zan Lynx
@ Zan Lynx: Votre argument est trop générique. La création de l'objet IAction est aussi difficile que de récupérer l'entier d'action pas plus fort ni plus facilement. Nous pouvons donc avoir une vraie conversation sans être trop générique. Considérons une calculatrice. Quelle est la différence de complexité ici? La réponse est zéro. Comme toutes les actions pré-créées. Vous obtenez la contribution de l'utilisateur et c'est déjà une action.
Martin York
3
@Martin: Vous supposez une application de calculatrice graphique. Prenons plutôt une application de calculatrice à clavier écrite pour C ++ sur un système intégré. Vous avez maintenant un entier de code de balayage provenant d'un registre matériel. Maintenant, qu'est-ce qui est moins complexe?
Zan Lynx
2
@Martin: Vous ne voyez pas comment integer -> table de consultation -> la création d'un nouvel objet -> call virtual function est plus compliqué que integer -> switch -> function? Comment voyez-vous pas cela?
Zan Lynx
2
@ Martin: Peut-être que je le ferai. En attendant, expliquez comment vous obtenez l'objet IAction pour appeler action () à partir d'un entier sans table de recherche.
Zan Lynx
4

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.

Luke Graham
la source
3

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.

Homde
la source
3

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

Steven A. Lowe
la source
3

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 gotoinstructions afin d'accélérer le code dans des domaines critiques . C'est la clé: les chemins critiques .

Il proposait d'utiliser gotopour 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 switchdé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.

A-t-il raison?

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 elle methodest 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.

Est-ce qu'il parle juste son cul?

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
2

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.

FrustratedWithFormsDesigner
la source
1
Outre les tests de minutage, un vidage de code peut aider à montrer le fonctionnement de la répartition des méthodes C ++ (et C #).
S.Lott
2

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, Catet Elephantet parfois Doget que vous Catavez le même cas, vous pouvez les faire hériter d'une classe abstraite DomesticAnimalet 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 ExtractConstantsou ExtractSymbols. J'ai utilisé cette approche pour un interprète BASIC jouet.

Jwg
la source
Un commutateur peut également hériter de comportements, à travers son cas par défaut. "... étend BaseOperationVisitor" devient "par défaut: BaseOperation (noeud)"
Samuel Danielson
0

"Nous devrions oublier les petites économies, disons environ 97% du temps: une optimisation prématurée est la racine de tout mal"

Donald Knuth

Thorsten Müller
la source
0

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.

Dirk Holsopple
la source
1
Manquant "non"? En C #, pas tous les appels de fonction sont virtuels. C # n'est pas Java.
Ben Voigt
0

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.

David Arno
la source
0

Il ne parle pas nécessairement de ses fesses. Au moins en C et C ++, les switchinstructions 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 switchdé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 switchdé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 le switch.

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 switchdéclaration analogique . Si vous savez avec certitude qu'il n'y en aura qu'un seul, une switchinstruction 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'une caseinstruction à 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