Warum einen OO-Ansatz anstelle einer riesigen "switch" -Anweisung verwenden?


59

Ich arbeite in einem .Net-, C # -Shop und habe einen Kollegen, der immer wieder darauf besteht, dass wir in unserem Code Riesen-Switch-Anweisungen mit vielen "Fällen" anstelle von objektorientierteren Ansätzen verwenden sollten. Sein Argument geht konsequent auf die Tatsache zurück, dass eine Switch-Anweisung zu einer "CPU-Sprungtabelle" kompiliert wird und daher die schnellste Option ist (auch wenn unserem Team in anderen Dingen gesagt wird, dass uns die Geschwindigkeit egal ist).

Ich habe ehrlich gesagt kein Argument dagegen ... weil ich nicht weiß, wovon zum Teufel er spricht.
Hat er recht
Spricht er nur seinen Arsch aus?
Ich versuche nur hier zu lernen.


7
Sie können überprüfen, ob er Recht hat, indem Sie .NET Reflector verwenden, um den Assemblycode zu überprüfen und nach der "CPU-Sprungtabelle" zu suchen.
FrustratedWithFormsDesigner

5
"Switch-Anweisung wird in eine" cpu-Sprungtabelle "kompiliert. Dies gilt auch für die Worst-Case-Methode, bei der alle rein virtuellen Funktionen verteilt werden. Keine virtuellen Funktionen werden direkt
eingebunden

64
Code sollte für MENSCHEN geschrieben werden, nicht für Maschinen, sonst würden wir einfach alles in der Montage machen.
maple_shaft

8
Wenn er so eine Nudel ist, zitiere Knuth zu ihm: "Wir sollten in 97% der Fälle kleine Wirkungsgrade vergessen: Vorzeitige Optimierung ist die Wurzel allen Übels."
DaveE

12
Wartbarkeit. Haben Sie noch Fragen mit einem Wort Antworten, bei denen ich Ihnen helfen kann?
Matt Ellen

Antworten:


48

Er ist wahrscheinlich ein alter C-Hacker und er redet aus seinem Arsch. .Net ist nicht C ++; Der .Net-Compiler wird immer besser und die meisten cleveren Hacks sind kontraproduktiv, wenn nicht heute, dann in der nächsten .Net-Version. Kleine Funktionen sind vorzuziehen, da .Net JIT-s jede Funktion einmal vor ihrer Verwendung ausführt. Wenn also einige Fälle während eines Lebenszyklus eines Programms nie betroffen sind, entstehen bei der JIT-Kompilierung keine Kosten. Auf jeden Fall sollten keine Optimierungen vorgenommen werden, wenn die Geschwindigkeit kein Problem darstellt. Schreiben Sie zuerst für den Programmierer, dann für den Compiler. Ihr Kollege lässt sich nicht leicht überzeugen, daher würde ich empirisch nachweisen, dass besser organisierter Code tatsächlich schneller ist. Ich würde eines seiner schlechtesten Beispiele auswählen, sie besser umschreiben und dann sicherstellen, dass Ihr Code schneller ist. Cherry-Pick, wenn Sie müssen. Dann starte es ein paar Millionen mal, profiliere und zeige es ihm.

BEARBEITEN

Bill Wagner schrieb:

Punkt 11: Die Anziehungskraft kleiner Funktionen verstehen (Effektive C # -Zweitausgabe) Beachten Sie, dass das Übersetzen Ihres C # -Codes in maschinenausführbaren Code ein zweistufiger Prozess ist. Der C # -Compiler generiert eine AWL, die in Assemblys bereitgestellt wird. Der JIT-Compiler generiert nach Bedarf Maschinencode für jede Methode (oder Gruppe von Methoden, wenn Inlining beteiligt ist). Kleine Funktionen erleichtern dem JIT-Compiler die Amortisation dieser Kosten. Kleine Funktionen sind auch eher Kandidaten für Inlining. Es ist nicht nur Kleinheit: Einfacherer Kontrollfluss ist genauso wichtig. Weniger Kontrollzweige in Funktionen erleichtern es dem JIT-Compiler, Variablen zu registrieren. Es ist nicht nur eine gute Übung, klareren Code zu schreiben. So erstellen Sie zur Laufzeit effizienteren Code.

EDIT2:

Also ... anscheinend ist eine switch-Anweisung schneller und besser als eine Reihe von if / else-Anweisungen, da ein Vergleich logarithmisch und ein anderer linear ist. http://sequence-points.blogspot.com/2007/10/why-is-switch-statement-faster-than-if.html

Mein bevorzugter Ansatz, um eine große switch-Anweisung zu ersetzen, ist ein Dictionary (oder manchmal sogar ein Array, wenn ich Enums oder kleine Ints einschalte), das Werte auf Funktionen abbildet, die als Antwort darauf aufgerufen werden. Dies zwingt einen dazu, eine Menge gemeiner Spaghetti zu entfernen, aber das ist eine gute Sache. Eine große switch-Anweisung ist normalerweise ein Wartungsalptraum. Also ... bei Arrays und Wörterbüchern wird die Suche eine konstante Zeit in Anspruch nehmen und es wird wenig zusätzlicher Speicher verschwendet.

Ich bin immer noch nicht davon überzeugt, dass die switch-Anweisung besser ist.


47
Mach dir keine Sorgen, es schneller zu beweisen. Dies ist eine vorzeitige Optimierung. Die Millisekunde, die Sie möglicherweise speichern, ist nichts im Vergleich zu dem Index, den Sie vergessen haben, der Datenbank hinzuzufügen, die Sie 200 ms kostet. Du schlägst den falschen Kampf.
Rein Henrichs

27
@Job was ist, wenn er tatsächlich Recht hat? Der Punkt ist nicht, dass er sich irrt, der Punkt ist, dass er Recht hat und es spielt keine Rolle .
Rein Henrichs

2
Auch wenn er in 100% der Fälle Recht hatte, verschwendet er immer noch unsere Zeit.
Jeremy

6
Ich möchte meine Augen ausstechen und versuchen, die von Ihnen verlinkte Seite zu lesen.
AttackingHobo

3
Was ist mit dem Hass auf C ++? Die C ++ - Compiler werden ebenfalls besser, und große Switches sind in C ++ genauso schlecht wie in C # und aus genau demselben Grund. Wenn Sie von ehemaligen C ++ - Programmierern umgeben sind, die Ihnen Kummer bereiten, dann nicht, weil sie C ++ - Programmierer sind, sondern weil sie schlechte Programmierer sind.
Sebastian Redl

39

Sofern Ihr Kollege nicht nachweisen kann, dass diese Änderung im Maßstab der gesamten Anwendung einen messbaren Nutzen bringt, steht sie Ihrem Ansatz (dh dem Polymorphismus), der tatsächlich einen solchen Nutzen bietet, unterlegen: die Wartbarkeit.

Die Mikrooptimierung sollte nur durchgeführt werden, nachdem Engpässe behoben wurden. Vorzeitige Optimierung ist die Wurzel allen Übels .

Geschwindigkeit ist quantifizierbar. Es gibt wenig nützliche Informationen in "Ansatz A ist schneller als Ansatz B". Die Frage ist " Wie viel schneller? ".


2
Absolut wahr. Niemals behaupten, dass etwas schneller ist, immer messen. Und messen Sie nur, wenn dieser Teil der Anwendung der Leistungsengpass ist.
Kilian Foth

6
-1 für "Vorzeitige Optimierung ist die Wurzel allen Übels." Bitte zeigen Sie das gesamte Zitat an, nicht nur einen Teil, der Knuths Meinung widerspricht.
Alternative

2
@mathepic: Ich habe dies absichtlich nicht als Zitat präsentiert. Dieser Satz ist, wie er ist, meine persönliche Meinung, obwohl er natürlich nicht meine Schöpfung ist. Obwohl angemerkt werden kann, dass die Jungs von c2 genau diesen Teil als die Kernweisheit zu betrachten scheinen.
back2dos

8
@alternative Das volle Knuth-Zitat "Es besteht kein Zweifel, dass der Gral der Effizienz zu Missbrauch führt. Programmierer verschwenden enorm viel Zeit damit, über die Geschwindigkeit unkritischer Teile ihrer Programme nachzudenken oder sich Gedanken zu machen, und diese Effizienzversuche haben tatsächlich eine Starke negative Auswirkungen, wenn Debugging und Wartung in Betracht gezogen werden. Wir sollten kleine Effizienzvorteile vergessen, sagen wir in 97% der Fälle: Vorzeitige Optimierung ist die Wurzel allen Übels. " Beschreibt den Mitarbeiter des OP perfekt. IMHO back2dos fasste das Zitat gut mit "vorzeitige Optimierung ist die Wurzel allen Übels"
MarkJ

2
@ MarkJ 97% der Zeit
Alternative

27

Wen kümmert es, wenn es schneller ist?

Wenn Sie keine Echtzeit-Software schreiben, ist es unwahrscheinlich, dass die winzige Geschwindigkeit, die Sie möglicherweise durch völlig verrückte Aktionen erzielen, einen großen Unterschied für Ihren Kunden bedeutet. Ich würde mich nicht einmal an der Geschwindigkeitsfront mit dieser messen, dieser Typ wird sich offensichtlich keine Argumente zu diesem Thema anhören.

Wartbarkeit ist jedoch das Ziel des Spiels, und eine riesige switch-Anweisung ist nicht einmal geringfügig wartbar. Wie erklären Sie die verschiedenen Pfade durch den Code einem neuen Spieler? Die Dokumentation muss so lang sein wie der Code selbst!

Außerdem haben Sie dann die völlige Unfähigkeit, Unit-Tests effektiv durchzuführen (zu viele mögliche Pfade, ganz zu schweigen vom wahrscheinlichen Mangel an Schnittstellen usw.), was Ihren Code noch weniger wartbar macht.

[Interessanterweise bietet der JITter eine bessere Leistung bei kleineren Methoden, sodass riesige switch-Anweisungen (und deren inhärent große Methoden) Ihrer Geschwindigkeit in großen Assemblys schaden (IIRC).]


1
+ ein riesiges Beispiel für vorzeitige Optimierung.
ShaneC

Auf jeden Fall das.
DeadMG

+1 für 'eine riesige switch-Anweisung ist nicht einmal leicht wartbar'
Korey Hinton

2
Eine riesige switch-Anweisung ist für den neuen Benutzer viel einfacher zu verstehen: Alle möglichen Verhaltensweisen werden direkt in einer übersichtlichen Liste zusammengefasst. Indirekte Aufrufe sind äußerst schwierig zu verfolgen, im schlimmsten Fall (Funktionszeiger) müssen Sie die gesamte Codebasis nach Funktionen der richtigen Signatur durchsuchen, und virtuelle Aufrufe sind nur wenig besser (Suche nach Funktionen mit dem richtigen Namen und der richtigen Signatur und) durch Vererbung verbunden). Bei der Wartbarkeit geht es jedoch nicht darum, schreibgeschützt zu sein.
Ben Voigt

14

Schritt weg von der switch-Anweisung ...

Diese Art von switch-Anweisung sollte wie eine Pest gemieden werden, da sie das Open Closed-Prinzip verletzt . Es zwingt das Team, Änderungen am vorhandenen Code vorzunehmen, wenn neue Funktionen hinzugefügt werden müssen, anstatt nur neuen Code hinzuzufügen.


11
Das kommt mit einer Einschränkung. Es gibt Operationen (Funktionen / Methoden) und Typen. Wenn Sie eine neue Operation hinzufügen, müssen Sie nur den Code an einer Stelle für die switch-Anweisungen ändern (fügen Sie eine neue Funktion mit switch-Anweisung hinzu), diese Methode müssten Sie jedoch allen Klassen im OO-Fall hinzufügen (verletzt open) geschlossenes Prinzip). Wenn Sie neue Typen hinzufügen, müssen Sie jede switch-Anweisung berühren, aber im OO-Fall fügen Sie einfach eine weitere Klasse hinzu. Um eine fundierte Entscheidung zu treffen, müssen Sie daher wissen, ob Sie den vorhandenen Typen weitere Operationen hinzufügen oder weitere Typen hinzufügen.
Scott Whitlock

3
Wenn Sie in einem OO-Paradigma mehr Operationen zu vorhandenen Typen hinzufügen müssen, ohne das OCP zu verletzen, dann ist dies meines Erachtens das Ziel des Besuchermusters.
Scott Whitlock

3
@Martin - nennen Sie den Namen, wenn Sie so wollen, aber das ist ein bekannter Kompromiss. Ich verweise Sie auf Clean Code von RC Martin. Er überarbeitet seinen Artikel über OCP und erklärt, was ich oben skizziert habe. Sie können nicht gleichzeitig für alle zukünftigen Anforderungen entwerfen. Sie müssen sich entscheiden, ob mehr Operationen oder mehr Typen hinzugefügt werden sollen. OO bevorzugt das Hinzufügen von Typen. Sie können OO verwenden, um weitere Operationen hinzuzufügen, wenn Sie Operationen als Klassen modellieren. Dies scheint jedoch in das Besuchermuster zu gelangen, das seine eigenen Probleme aufweist (insbesondere Overhead).
Scott Whitlock

8
@Martin: Hast du schon mal einen Parser geschrieben? Es ist durchaus üblich, große Switch-Cases zu haben, die das nächste Token im Lookahead-Puffer einschalten. Sie könnten diese Schalter durch virtuelle Funktionsaufrufe für das nächste Token ersetzen, aber das wäre ein Alptraum für die Wartung. Es ist selten, aber manchmal ist das Schaltergehäuse die bessere Wahl, da es Code enthält, der in unmittelbarer Nähe zusammen gelesen / geändert werden sollte.
Nikie

1
@Martin: Sie haben Wörter wie "never", "ever" und "Poppycock" verwendet, also habe ich angenommen, Sie sprechen ausnahmslos über alle Fälle, nicht nur über den häufigsten Fall. (Und übrigens: Die Leute schreiben immer noch Parser von Hand. Zum Beispiel wird der CPython-Parser immer noch von Hand geschrieben, IIRC.)
nikie

8

Ich habe den Albtraum überlebt, der als massive endliche Zustandsmaschine bekannt ist, die durch massive Schaltanweisungen manipuliert wird. Schlimmer noch, in meinem Fall umfasste der FSM drei C ++ - DLLs und es war ganz klar, dass der Code von jemandem geschrieben wurde, der sich mit C auskennt.

Die Metriken, um die Sie sich kümmern müssen, sind:

  • Geschwindigkeit, mit der Änderungen vorgenommen werden
  • Geschwindigkeit, mit der das Problem gefunden wird, wenn es auftritt

Ich erhielt die Aufgabe, eine neue Funktion zu diesem Satz von DLLs hinzuzufügen, und konnte das Management davon überzeugen, dass es genauso lange dauern würde, die 3 DLLs als eine ordnungsgemäß objektorientierte DLL umzuschreiben, wie es für mich wäre, Affen-Patches zu erstellen und die Jury rüstet die Lösung für das, was bereits da war. Das Umschreiben war ein großer Erfolg, da es nicht nur die neue Funktionalität unterstützte, sondern auch einfacher zu erweitern war. Tatsächlich würde eine Aufgabe, die normalerweise eine Woche in Anspruch nimmt, um sicherzustellen, dass Sie nichts kaputtmachen, einige Stunden in Anspruch nehmen.

Wie wäre es mit Ausführungszeiten? Es gab keine Geschwindigkeitserhöhung oder -verringerung. Um fair zu sein, wurde unsere Leistung von den Systemtreibern gedrosselt. Wenn die objektorientierte Lösung also langsamer wäre, würden wir es nicht wissen.

Was ist los mit massiven switch-Anweisungen für eine OO-Sprache?

  • Der Programmsteuerungsfluss wird von dem Objekt entfernt, zu dem er gehört, und außerhalb des Objekts platziert
  • Viele Punkte der externen Kontrolle führen zu vielen Punkten, die Sie überprüfen müssen
  • Es ist unklar, wo der Zustand gespeichert ist, insbesondere wenn sich der Schalter innerhalb einer Schleife befindet
  • Der schnellste Vergleich ist überhaupt kein Vergleich (Sie können die Notwendigkeit vieler Vergleiche mit einem guten objektorientierten Design vermeiden)
  • Es ist effizienter, durch Ihre Objekte zu iterieren und immer dieselbe Methode für alle Objekte aufzurufen, als den Code basierend auf dem Objekttyp oder der Aufzählung, die den Typ codiert, zu ändern.

8

Ich kaufe das Leistungsargument nicht; Alles dreht sich um die Pflege von Code.

ABER: Manchmal ist eine riesige switch-Anweisung einfacher zu verwalten (weniger Code) als eine Reihe kleiner Klassen, die virtuelle Funktionen einer abstrakten Basisklasse überschreiben. Wenn Sie beispielsweise einen CPU-Emulator implementieren würden, würden Sie die Funktionalität jeder Anweisung nicht in einer separaten Klasse implementieren. Sie würden sie lediglich in einen riesigen Schalter auf dem Opcode einfügen und möglicherweise Hilfsfunktionen für komplexere Anweisungen aufrufen.

Faustregel: Wenn der Wechsel auf dem TYPE ausgeführt wird, sollten Sie wahrscheinlich Vererbungs- und virtuelle Funktionen verwenden. Wenn der Wechsel für einen WERT eines festen Typs durchgeführt wird (z. B. den Anweisungs-Opcode wie oben), ist es in Ordnung, ihn so zu belassen, wie er ist.


5

Das kannst du mich nicht überzeugen:

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;
    }
}

Ist deutlich schneller als:

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();
}

Darüber hinaus ist die OO-Version einfacher zu warten.


8
Für einige Dinge und für kleinere Mengen von Aktionen ist die OO-Version viel doofer. Es muss eine Fabrik geben, um einen bestimmten Wert in eine IAction umzuwandeln. In vielen Fällen ist es viel lesbarer, stattdessen nur diesen Wert einzuschalten.
Zan Lynx

@Zan Lynx: Dein Argument ist zu allgemein. Das Erstellen des IAction-Objekts ist genauso schwierig wie das Abrufen der Action Integer-Zahl. So können wir ein echtes Gespräch führen, ohne generisch zu werden. Betrachten Sie einen Taschenrechner. Was ist der Unterschied in der Komplexität hier? Die Antwort ist Null. Wie alle vorgefertigten Aktionen. Sie erhalten die Eingabe vom Benutzer und es ist bereits eine Aktion.
Martin York

3
@Martin: Sie setzen eine GUI-Rechner-App voraus. Nehmen wir stattdessen eine Tastaturrechner-App, die für C ++ auf einem eingebetteten System geschrieben wurde. Jetzt haben Sie eine Ganzzahl mit Scan-Code aus einem Hardware-Register. Was ist nun weniger komplex?
Zan Lynx

2
@Martin: Sie sehen nicht, wie Integer -> Lookup-Tabelle -> Erstellung eines neuen Objekts -> Aufruf einer virtuellen Funktion komplizierter ist als Integer -> Schalter -> Funktion? Wie siehst du das nicht?
Zan Lynx

2
@ Martin: Vielleicht werde ich. Erklären Sie in der Zwischenzeit, wie Sie das IAction-Objekt dazu bringen, action () von einer Ganzzahl ohne Nachschlagetabelle aufzurufen .
Zan Lynx

4

Er hat Recht, dass der resultierende Maschinencode wahrscheinlich effizienter sein wird. Der Compiler Essential wandelt eine switch-Anweisung in eine Reihe von Tests und Verzweigungen um, bei denen es sich um relativ wenige Anweisungen handelt. Es besteht eine hohe Wahrscheinlichkeit, dass der Code, der sich aus abstrakteren Ansätzen ergibt, mehr Anweisungen erfordert.

JEDOCH : Mit ziemlicher Sicherheit muss sich Ihre Anwendung nicht um diese Art der Mikrooptimierung kümmern, oder Sie würden .net gar nicht erst verwenden. Wenn es sich nicht um sehr eingeschränkte Embedded-Anwendungen oder um CPU-intensive Arbeit handelt, sollten Sie den Compiler immer mit der Optimierung beauftragen. Konzentrieren Sie sich darauf, sauberen, wartbaren Code zu schreiben. Dies ist fast immer von weitaus größerem Wert als ein paar Zehntelsekunden Ausführungszeit.


3

Ein Hauptgrund für die Verwendung von Klassen anstelle von switch-Anweisungen besteht darin, dass switch-Anweisungen dazu neigen, zu einer großen Datei mit viel Logik zu führen. Dies ist sowohl ein Alptraum bei der Wartung als auch ein Problem bei der Quellcodeverwaltung, da Sie diese große Datei auschecken und bearbeiten müssen, anstatt andere kleinere Klassendateien zu verwenden


3

Eine switch-Anweisung im OOP-Code ist ein starker Hinweis auf fehlende Klassen

Probieren Sie es in beide Richtungen und führen Sie einige einfache Geschwindigkeitstests durch. Die Chancen stehen gut, dass der Unterschied nicht signifikant ist. Wenn dies der Fall ist und der Code zeitkritisch ist, behalten Sie die switch-Anweisung bei


3

Normalerweise hasse ich das Wort "vorzeitige Optimierung", aber das stinkt danach. Es ist erwähnenswert, dass Knuth dieses berühmte Zitat im Zusammenhang mit der Verwendung von gotoAnweisungen verwendet hat, um den Code in kritischen Bereichen zu beschleunigen . Das ist der Schlüssel: kritische Pfade.

Er gotoschlug vor, den Code zu beschleunigen, warnte aber vor den Programmierern, die diese Art von Dingen auf der Grundlage von Aberglauben und Aberglauben für Code ausführen möchten, der nicht einmal kritisch ist.

Zu bevorzugen switchAussagen so weit wie möglich gleichmäßig über einen Code - Basis (ob eine schwere Last gehandhabt wird) ist das klassische Beispiel dafür , was Knuth nennt die „Penny-weise und Pfund-dumm“ Programmierer, der den ganzen Tag kämpfen verbringt behalten ihre „optimiert "Code, der sich als Ergebnis des Versuchs, Pennies über Pfund zu sparen, in einen Debug - Albtraum verwandelte. Ein solcher Code ist selten zu warten, geschweige denn überhaupt effizient.

Hat er recht

Er hat von der Grundperspektive der Effizienz her Recht. Meines Wissens kann kein Compiler polymorphen Code mit Objekten und dynamischem Versand besser optimieren als eine switch-Anweisung. Sie werden nie mit einer LUT oder einer Sprungtabelle zu Inline-Code aus polymorphem Code enden, da dieser Code in der Regel als Optimierungsbarriere für den Compiler dient (er weiß erst zum Zeitpunkt des dynamischen Versands, welche Funktion aufzurufen ist) tritt ein).

Es ist sinnvoller, nicht an diese Kosten in Form von Sprungtabellen zu denken, sondern eher in Form der Optimierungsbarriere. Aus Gründen des Polymorphismus kann Base.method()der Compiler beim Aufrufen nicht erkennen, welche Funktion tatsächlich aufgerufen wird, wenn sie methodvirtuell ist, nicht versiegelt und überschrieben werden kann. Da es nicht weiß, welche Funktion tatsächlich im Voraus aufgerufen wird, kann es den Funktionsaufruf nicht optimieren und mehr Informationen für Optimierungsentscheidungen verwenden, da es nicht wirklich weiß, auf welche Funktion zugegriffen wird die Zeit, zu der der Code kompiliert wird.

Optimierer sind am besten geeignet, wenn sie einen Blick auf einen Funktionsaufruf werfen und Optimierungen vornehmen können, die entweder den Anrufer und den Angerufenen vollständig reduzieren oder zumindest den Anrufer optimieren, um mit dem Angerufenen am effizientesten zu arbeiten. Sie können das nicht tun, wenn sie nicht wissen, welche Funktion tatsächlich im Voraus aufgerufen wird.

Spricht er nur seinen Arsch aus?

Die Verwendung dieser Kosten, die sich oft auf ein paar Cent belaufen, um die Umwandlung in einen einheitlich angewendeten Kodierungsstandard zu rechtfertigen, ist im Allgemeinen sehr töricht, insbesondere für Orte, die Erweiterungsbedarf haben. Das ist die Hauptsache, auf die Sie bei echten vorzeitigen Optimierern achten sollten: Sie möchten geringfügige Leistungsprobleme in Codierungsstandards umwandeln, die auf einer Codebasis einheitlich angewendet werden, ohne auf die Wartbarkeit zu achten.

Ich ärgere mich ein wenig über das Zitat "alter C-Hacker", das in der akzeptierten Antwort verwendet wird, da ich einer von denen bin. Nicht jeder, der seit Jahrzehnten mit sehr begrenzter Hardware programmiert, hat sich in einen vorzeitigen Optimierer verwandelt. Dennoch habe ich auch mit denen zusammengearbeitet. Aber diese Typen messen niemals Dinge wie Verzweigungsfehlvorhersagen oder Cache-Fehler, sie denken, sie wissen es besser und stützen ihre Vorstellung von Ineffizienz auf eine komplexe Produktionscodebasis, die auf Aberglauben basiert, die heute nicht zutreffen und manchmal nie zutreffen. Menschen, die wirklich in leistungskritischen Bereichen gearbeitet haben, verstehen oft, dass eine effektive Optimierung eine effektive Priorisierung ist, und der Versuch, einen die Wartbarkeit verschlechternden Kodierungsstandard zu verallgemeinern, um ein paar Cent zu sparen, ist eine sehr ineffektive Priorisierung.

Pennies sind wichtig, wenn Sie eine billige Funktion haben, die nicht so viel Arbeit leistet, was in einer sehr engen, leistungskritischen Schleife milliardenfach genannt wird. In diesem Fall sparen wir am Ende 10 Millionen Dollar. Es lohnt sich nicht, ein paar Cent zu rasieren, wenn Sie eine Funktion haben, die zweimal aufgerufen wird und für die allein der Körper Tausende von Dollar kostet. Es ist nicht ratsam, beim Autokauf um ein paar Cent zu feilschen. Es lohnt sich, um ein paar Cent zu feilschen, wenn Sie eine Million Dosen Soda von einem Hersteller kaufen. Der Schlüssel zu einer effektiven Optimierung liegt darin, diese Kosten im richtigen Kontext zu verstehen. Jemand, der versucht, bei jedem Einkauf ein paar Cent zu sparen und den anderen vorschlägt, um ein paar Cent zu feilschen, egal was er kauft, ist kein erfahrener Optimierer.


2

Es hört sich so an, als wäre Ihr Mitarbeiter sehr um die Leistung besorgt. Es kann sein, dass in einigen Fällen eine große Gehäuse- / Switch-Struktur eine schnellere Leistung erbringt, aber hoffentlich würden Sie ein Experiment durchführen, indem Sie Timing-Tests für die OO-Version und die Switch- / Case-Version durchführen. Ich vermute, dass die OO-Version weniger Code enthält und einfacher zu folgen, zu verstehen und zu warten ist. Ich würde zunächst für die OO-Version argumentieren (da Wartung / Lesbarkeit anfangs wichtiger sein sollte) und die Switch / Case-Version nur dann berücksichtigen, wenn die OO-Version schwerwiegende Leistungsprobleme aufweist und gezeigt werden kann, dass ein Switch / Case einen Fehler verursacht deutliche Verbesserung.


1
Zusammen mit Timing-Tests kann ein Code-Dump zeigen, wie das Dispatching von C ++ - (und C # -) Methoden funktioniert.
S.Lott

2

Ein Vorteil der Wartbarkeit von Polymorphismus, den niemand erwähnt hat, ist, dass Sie Ihren Code mithilfe der Vererbung viel besser strukturieren können, wenn Sie immer die gleiche Liste von Fällen aktivieren, aber manchmal mehrere Fälle auf die gleiche Art und Weise und zu einem bestimmten Zeitpunkt behandelt werden nicht

Z.B. wenn Sie zwischen wechseln Dog, Catund Elephant, und manchmal Dogund Cathaben den gleichen Fall, können Sie sie beide von einer abstrakten Klasse erben machen DomesticAnimalund jene Funktion in der abstrakten Klasse setzen.

Außerdem war ich überrascht, dass einige Leute einen Parser als Beispiel dafür verwendeten, wo Sie keinen Polymorphismus verwenden würden. Für einen baumartigen Parser ist dies definitiv der falsche Ansatz. Wenn Sie jedoch eine Art Assembly haben, bei der jede Zeile etwas unabhängig ist, und mit einem Opcode beginnen, der angibt, wie der Rest der Zeile interpretiert werden soll, würde ich Polymorphismus verwenden und eine Fabrik. Jede Klasse kann Funktionen wie ExtractConstantsoder implementieren ExtractSymbols. Ich habe diesen Ansatz für einen Spielzeug-BASIC-Interpreter verwendet.


Ein Switch kann über seinen Standardfall auch Verhalten erben. "... erweitert BaseOperationVisitor" wird zu "default: BaseOperation (node)"
Samuel Danielson

0

"Wir sollten kleine Wirkungsgrade vergessen, sagen wir in 97% der Fälle: Vorzeitige Optimierung ist die Wurzel allen Übels."

Donald Knuth


0

Auch wenn dies nicht schlecht für die Wartbarkeit war, glaube ich nicht, dass es für die Leistung besser sein wird. Ein virtueller Funktionsaufruf ist einfach eine zusätzliche Indirektion (wie der beste Fall für eine switch-Anweisung), sodass selbst in C ++ die Leistung ungefähr gleich sein sollte. In C #, in dem alle Funktionsaufrufe virtuell sind, sollte die switch-Anweisung schlechter sein, da Sie in beiden Versionen den gleichen Overhead für virtuelle Funktionsaufrufe haben.


1
Fehlt "nicht"? In C # sind nicht alle Funktionsaufrufe virtuell. C # ist nicht Java.
Ben Voigt

0

Ihr Kollege spricht nicht von seinem Hintern, so weit der Kommentar zu Sprungtischen geht. Indem er dies jedoch verwendet, um das Schreiben von schlechtem Code zu rechtfertigen, geht er schief.

Der C # -Compiler konvertiert switch-Anweisungen mit nur wenigen Fällen in eine Reihe von if / else-Anweisungen. Dies ist also nicht schneller als die Verwendung von if / else. Der Compiler konvertiert größere switch-Anweisungen in ein Dictionary (die Sprungtabelle, auf die sich Ihr Kollege bezieht). Weitere Informationen finden Sie in dieser Antwort auf eine Stapelüberlauf-Frage zum Thema .

Eine große switch-Anweisung ist schwer zu lesen und zu pflegen. Ein Wörterbuch mit "Fällen" und Funktionen ist viel einfacher zu lesen. Da dies der Fall ist, sind Sie und Ihr Kollege gut beraten, Wörterbücher direkt zu verwenden.


0

Er redet nicht unbedingt aus seinem Arsch. Zumindest in C und C ++ können switchAnweisungen optimiert werden, um Tabellen zu überspringen, während ich es noch nie mit einem dynamischen Versand in einer Funktion gesehen habe, die nur Zugriff auf einen Basiszeiger hat. Zumindest erfordert letzteres einen viel intelligenteren Optimierer, der viel mehr umgebenden Code betrachtet, um genau herauszufinden, welcher Subtyp von einem virtuellen Funktionsaufruf über einen Basiszeiger / eine Basisreferenz verwendet wird.

Darüber hinaus dient der dynamische Versand häufig als "Optimierungsbarriere", was bedeutet, dass der Compiler häufig nicht in der Lage ist, Code inline zu schreiben und Register optimal zuzuweisen, um Stapelverluste und all diese ausgefallenen Dinge zu minimieren, da er nicht herausfinden kann, was Die virtuelle Funktion wird über den Basiszeiger aufgerufen, um sie einzubinden und ihre gesamte Optimierungsmagie auszuführen. Ich bin mir nicht sicher, ob Sie wollen, dass der Optimierer so intelligent ist und versucht, indirekte Funktionsaufrufe zu optimieren, da dies möglicherweise dazu führen kann, dass viele Codezweige separat in einem bestimmten Aufrufstapel generiert werden müssen (eine Funktion, die Aufrufe haben foo->f()würden) einen völlig anderen Maschinencode als den aufrufenden zu erzeugenbar->f() Durch einen Basiszeiger und die Funktion, die diese Funktion aufruft, müsste dann zwei oder mehr Codeversionen generieren, und so weiter - die Menge des generierten Maschinencodes wäre explosiv - vielleicht nicht so schlecht mit einer Ablaufverfolgungs-JIT, die generiert den Code im laufenden Betrieb, während er über Hot-Execution-Pfade nachverfolgt wird.

Trotzdem, wie viele Antworten bestätigt haben, ist dies ein schlechter Grund, eine Schiffsladung von switchAussagen zu bevorzugen, auch wenn sie geringfügig schneller sind. Wenn es um Mikroeffizienzen geht, haben Verzweigungen und Inlining im Vergleich zu Speicherzugriffsmustern normalerweise eine relativ niedrige Priorität.

Trotzdem bin ich mit einer ungewöhnlichen Antwort hier reingesprungen. Ich möchte die Wartbarkeit von switchAussagen gegenüber einer polymorphen Lösung begründen, wenn und nur dann, wenn Sie sicher sind, dass es nur einen Ort geben wird, an dem die Aussagen durchgeführt werden müssen switch.

Ein Paradebeispiel ist ein zentraler Event-Handler. In diesem Fall gibt es im Allgemeinen nicht viele Orte, an denen Ereignisse verarbeitet werden, sondern nur einen (warum "zentral"). In diesen Fällen profitieren Sie nicht von der Erweiterbarkeit, die eine polymorphe Lösung bietet. Eine polymorphe Lösung ist vorteilhaft, wenn es viele Stellen gibt, an denen die analoge switchAussage möglich ist. Wenn Sie sicher sind, dass es nur eine geben wird, kann eine switchAnweisung mit 15 Fällen viel einfacher sein als das Entwerfen einer Basisklasse, die von 15 Subtypen mit überschriebenen Funktionen geerbt wird, und einer Factory, um sie zu instanziieren, um dann in einer Funktion verwendet zu werden im gesamten System. In diesen Fällen ist das Hinzufügen eines neuen Untertyps viel mühsamer als das Hinzufügen einer caseAnweisung zu einer Funktion. Wenn überhaupt, würde ich für die Wartbarkeit argumentieren, nicht die Leistung,switch Aussagen in diesem einen besonderen Fall, in dem Sie überhaupt nicht von der Erweiterbarkeit profitieren.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.