Was ist die beste Vorgehensweise für die Verwendung einer switch
Anweisung im Vergleich zur Verwendung einer if
Anweisung für 30 unsigned
Aufzählungen, bei denen etwa 10 eine erwartete Aktion haben (dies ist derzeit dieselbe Aktion) ? Leistung und Platz müssen berücksichtigt werden, sind jedoch nicht kritisch. Ich habe das Snippet abstrahiert, also hasse mich nicht für die Namenskonventionen.
switch
Aussage:
// numError is an error enumeration type, with 0 being the non-error case
// fire_special_event() is a stub method for the shared processing
switch (numError)
{
case ERROR_01 : // intentional fall-through
case ERROR_07 : // intentional fall-through
case ERROR_0A : // intentional fall-through
case ERROR_10 : // intentional fall-through
case ERROR_15 : // intentional fall-through
case ERROR_16 : // intentional fall-through
case ERROR_20 :
{
fire_special_event();
}
break;
default:
{
// error codes that require no additional action
}
break;
}
if
Aussage:
if ((ERROR_01 == numError) ||
(ERROR_07 == numError) ||
(ERROR_0A == numError) ||
(ERROR_10 == numError) ||
(ERROR_15 == numError) ||
(ERROR_16 == numError) ||
(ERROR_20 == numError))
{
fire_special_event();
}
Antworten:
Schalter verwenden.
Im schlimmsten Fall generiert der Compiler denselben Code wie eine If-else-Kette, sodass Sie nichts verlieren. Wenn Sie Zweifel haben, geben Sie die häufigsten Fälle zuerst in die switch-Anweisung ein.
Im besten Fall findet der Optimierer möglicherweise einen besseren Weg, um den Code zu generieren. Ein Compiler erstellt häufig einen binären Entscheidungsbaum (speichert Vergleiche und Sprünge im Durchschnitt) oder erstellt einfach eine Sprungtabelle (funktioniert überhaupt ohne Vergleiche).
quelle
if
-else
Ketten in Vorlage Meta - Programmierung und die Schwierigkeiten bei der Erzeugung vonswitch
case
Ketten, kann diese Zuordnung wichtiger worden. (und ja, alter Kommentar, aber das Web ist für immer oder zumindest bis zum nächsten Dienstag)Für den Sonderfall, den Sie in Ihrem Beispiel angegeben haben, lautet der klarste Code wahrscheinlich:
Dies verschiebt das Problem natürlich nur in einen anderen Bereich des Codes, aber jetzt haben Sie die Möglichkeit, diesen Test wiederzuverwenden. Sie haben auch mehr Möglichkeiten, wie Sie es lösen können. Sie könnten std :: set verwenden, zum Beispiel:
Ich schlage nicht vor, dass dies die beste Implementierung von RequiresSpecialEvent ist, nur dass es eine Option ist. Sie können weiterhin einen Schalter oder eine if-else-Kette oder eine Nachschlagetabelle oder eine Bitmanipulation für den Wert verwenden. Je dunkler Ihr Entscheidungsprozess wird, desto mehr Wert erhalten Sie, wenn Sie ihn in einer isolierten Funktion haben.
quelle
const std::bitset<MAXERR> specialerror(initializer);
Verwenden Sie es mitif (specialerror[numError]) { fire_special_event(); }
. Wenn Sie die Grenzen überprüfen möchten,bitset::test(size_t)
wird eine Ausnahme für Werte außerhalb der Grenzen ausgelöst. (bitset::operator[]
überprüft nicht die Reichweite). cplusplus.com/reference/bitset/bitset/test . Dies wird wahrscheinlich eine vom Compiler generierte Sprungtabelle übertreffen, die implementiertswitch
, insb. in dem nicht speziellen Fall, in dem dies ein einzelner nicht genommener Zweig ist.std::set
, dachte ich, ich würde darauf hinweisen, dass es wahrscheinlich eine schlechte Wahl ist. Es stellt sich heraus, dass gcc bereits den OP-Code kompiliert, um eine Bitmap in einem 32-Bit-Sofort zu testen. godbolt: goo.gl/qjjv0e . gcc 5.2 wird dies sogar für dieif
Version tun . Neuere gcc verwenden außerdem den Bit-Test-Befehl,bt
anstatt ihn zu verschieben, um ein1
Bit an die richtige Stelle zu setzen und zu verwendentest reg, imm32
.Der Wechsel ist schneller.
Versuchen Sie einfach, 30 verschiedene Werte in einer Schleife einzugeben, und vergleichen Sie sie mit dem gleichen Code mithilfe des Schalters, um festzustellen, wie viel schneller der Schalter ist.
Jetzt hat der Switch ein echtes Problem : Der Switch muss zur Kompilierungszeit die Werte in jedem Fall kennen. Dies bedeutet, dass der folgende Code:
wird nicht kompiliert.
Die meisten Leute verwenden dann define (Aargh!), Und andere deklarieren und definieren konstante Variablen in derselben Kompilierungseinheit. Beispielsweise:
Am Ende muss der Entwickler also zwischen "Geschwindigkeit + Klarheit" und "Codekopplung" wählen.
(Nicht, dass ein Schalter nicht so geschrieben werden kann, dass er verdammt verwirrend ist ... Die meisten Schalter, die ich derzeit sehe, gehören zu dieser "verwirrenden" Kategorie "... Aber das ist eine andere Geschichte ...)
.
quelle
Der Compiler wird es trotzdem optimieren - wählen Sie den Schalter, da er am besten lesbar ist.
quelle
gcc
wird das nicht sicher tun (es gibt einen guten Grund dafür). Clang optimiert beide Fälle in eine binäre Suche. Siehe zum Beispiel dies .Der Switch, wenn auch nur zur besseren Lesbarkeit. Riesig, wenn Aussagen meiner Meinung nach schwerer zu pflegen und schwerer zu lesen sind.
FEHLER_01 : // absichtlicher
oder
(ERROR_01 == numError) ||
Letzteres ist fehleranfälliger und erfordert mehr Eingabe und Formatierung als das erste.
quelle
Code für die Lesbarkeit. Wenn Sie wissen möchten, was besser funktioniert, verwenden Sie einen Profiler, da Optimierungen und Compiler unterschiedlich sind und Leistungsprobleme selten dort auftreten, wo die Leute glauben, dass sie sind.
quelle
Verwenden Sie den Schalter, es ist das, wofür es ist und was Programmierer erwarten.
Ich würde jedoch die überflüssigen Falletiketten einfügen - nur damit sich die Leute wohl fühlen, habe ich versucht, mich daran zu erinnern, wann / welche Regeln für das Auslassen gelten.
Sie möchten nicht, dass der nächste Programmierer, der daran arbeitet, unnötig über Sprachdetails nachdenken muss (möglicherweise sind Sie es in ein paar Monaten!).
quelle
Compiler können wirklich gut optimieren
switch
. Aktuelle gcc ist auch gut in der Optimierung einer Reihe von Bedingungen in einemif
.Ich habe einige Testfälle auf Godbolt gemacht .
Wenn die
case
Werte eng zusammen gruppiert sind, sind gcc, clang und icc intelligent genug, um mithilfe einer Bitmap zu überprüfen, ob ein Wert einer der besonderen ist.zB gcc 5.2 -O3 kompiliert das
switch
to (und dasif
etwas sehr ähnliches):Beachten Sie, dass es sich bei der Bitmap um unmittelbare Daten handelt, sodass kein potenzieller Daten-Cache-Fehler beim Zugriff darauf oder eine Sprungtabelle auftritt.
gcc 4.9.2 -O3 kompiliert das
switch
zu einer Bitmap, macht das aber1U<<errNumber
mit mov / shift. Es kompiliert dieif
Version zu einer Reihe von Zweigen.Beachten Sie, wie 1 von subtrahiert wird
errNumber
(mitlea
, um diese Operation mit einer Bewegung zu kombinieren). Dadurch kann die Bitmap sofort in eine 32-Bit-Version eingepasst werden, wobei die 64-Bit-Version sofort vermieden wirdmovabsq
mehr Anweisungsbytes benötigt.Eine kürzere (im Maschinencode) Sequenz wäre:
(Die Nichtverwendung
jc fire_special_event
ist allgegenwärtig und ein Compiler-Fehler .)rep ret
wird in Zweigzielen und folgenden bedingten Zweigen zum Nutzen des alten AMD K8 und K10 (Pre-Bulldozer) verwendet: Was bedeutet "rep ret"? . Ohne diese Funktion funktioniert die Verzweigungsvorhersage auf diesen veralteten CPUs nicht so gut.bt
(Bittest) mit einem Register arg ist schnell. Es kombiniert die Arbeit, eine 1 umerrNumber
Bits nach links zu verschieben und a zu tuntest
, hat aber immer noch eine Latenz von 1 Zyklus und nur einen einzigen Intel-UOP. Mit einem Speicherargument ist es aufgrund seiner viel zu CISC-Semantik langsam: Mit einem Speicheroperanden für die "Bitfolge" wird die Adresse des zu testenden Bytes basierend auf dem anderen Arg (geteilt durch 8) berechnet und isn Es ist nicht auf den 1-, 2-, 4- oder 8-Byte-Block beschränkt, auf den der Speicheroperand zeigt.Aus den Befehlstabellen von Agner Fog geht hervor , dass ein Shift-Befehl mit variabler Anzahl langsamer ist als ein
bt
neuerer Intel-Befehl (2 Uops anstelle von 1, und Shift erledigt nicht alles, was sonst noch benötigt wird).quelle
Die Vorteile von switch () gegenüber anderen Fällen sind: - 1. Switch ist viel effizienter als if else, da jeder Fall nicht vom vorherigen Fall abhängt, im Gegensatz zu anderen Fällen, in denen einzelne Anweisungen auf wahre oder falsche Bedingungen überprüft werden müssen.
Wenn es eine Nr. Gibt. Bei den Werten für einen einzelnen Ausdruck ist die Groß- und Kleinschreibung flexibler, wenn dies der Fall ist, da in anderen Fällen die Beurteilung nur auf zwei Werten basiert, dh entweder wahr oder falsch.
Die Werte in switch sind benutzerdefiniert, während die Werte in if else case auf Einschränkungen basieren.
Im Fehlerfall können Anweisungen im Schalter leicht überprüft und korrigiert werden, was bei if else-Anweisungen vergleichsweise schwierig zu überprüfen ist.
Das Schaltergehäuse ist viel kompakter und leicht zu lesen und zu verstehen.
quelle
IMO ist dies ein perfektes Beispiel dafür, wofür Switch Fall-Through gemacht wurde.
quelle
Wenn Ihre Fälle in Zukunft wahrscheinlich gruppiert bleiben - wenn mehr als ein Fall einem Ergebnis entspricht - ist der Schalter möglicherweise leichter zu lesen und zu warten.
quelle
Sie funktionieren gleich gut. Die Leistung ist bei einem modernen Compiler ungefähr gleich.
Ich bevorzuge if-Anweisungen gegenüber case-Anweisungen, da sie lesbarer und flexibler sind. Sie können andere Bedingungen hinzufügen, die nicht auf numerischer Gleichheit basieren, z. B. "|| max <min". Aber für den einfachen Fall, den Sie hier gepostet haben, spielt es keine Rolle, tun Sie einfach das, was für Sie am besten lesbar ist.
quelle
Schalter ist definitiv bevorzugt. Es ist einfacher, sich die Liste der Fälle eines Switch anzusehen und sicher zu wissen, was er tut, als die lange if-Bedingung zu lesen.
Die Verdoppelung des
if
Zustands ist schwer für die Augen. Angenommen, einer der==
wurde geschrieben!=
; würdest du es bemerken Oder wenn eine Instanz von 'numError' 'nmuError' geschrieben wurde, die gerade kompiliert wurde?Ich würde im Allgemeinen lieber Polymorphismus anstelle des Schalters verwenden, aber ohne weitere Details des Kontexts ist es schwer zu sagen.
In Bezug auf die Leistung ist es am besten, einen Profiler zu verwenden, um die Leistung Ihrer Anwendung unter Bedingungen zu messen, die denen entsprechen, die Sie in freier Wildbahn erwarten. Andernfalls optimieren Sie wahrscheinlich am falschen Ort und auf die falsche Weise.
quelle
Ich bin mit der Kompatibilität der Switch-Lösung einverstanden, aber IMO entführen Sie den Switch hier.
Der Zweck des Schalters besteht darin, je nach Wert eine unterschiedliche Handhabung zu haben .
Wenn Sie Ihr Algo in Pseudocode erklären müssten, würden Sie ein if verwenden, weil es semantisch so ist: wenn Whatever_error dies tut ... Es
sei denn, Sie beabsichtigen, Ihren Code eines Tages so zu ändern, dass er für jeden Fehler einen bestimmten Code enthält Ich würde wenn verwenden .
quelle
Ich würde die if-Aussage aus Gründen der Klarheit und Konvention auswählen, obwohl ich sicher bin, dass einige anderer Meinung sind. Schließlich möchten Sie etwas tun,
if
eine Bedingung ist wahr! Einen Schalter mit einer Aktion zu haben, scheint ein wenig ... unnötig.quelle
Ich würde sagen, SWITCH verwenden. Auf diese Weise müssen Sie nur unterschiedliche Ergebnisse implementieren. Ihre zehn identischen Fälle können die Standardeinstellung verwenden. Sollte eine Änderung nur die Änderung explizit implementieren, müssen Sie die Standardeinstellung nicht bearbeiten. Es ist auch viel einfacher, Fälle zu einem SWITCH hinzuzufügen oder daraus zu entfernen, als IF und ELSEIF zu bearbeiten.
Vielleicht testen Sie sogar Ihren Zustand (in diesem Fall numerror) anhand einer Liste von Möglichkeiten, einem Array, damit Ihr SWITCH nicht einmal verwendet wird, es sei denn, es wird definitiv ein Ergebnis geben.
quelle
Da Sie nur 30 Fehlercodes haben, codieren Sie Ihre eigene Sprungtabelle und treffen dann alle Optimierungsentscheidungen selbst (der Sprung ist immer am schnellsten), anstatt zu hoffen, dass der Compiler das Richtige tut. Es macht auch den Code sehr klein (abgesehen von der statischen Deklaration der Sprungtabelle). Es hat auch den Nebeneffekt, dass Sie mit einem Debugger das Verhalten zur Laufzeit ändern können, wenn Sie dies benötigen, indem Sie die Tabellendaten direkt durchsuchen.
quelle
std::bitset<MAXERR> specialerror;
dann eineif (specialerror[err]) { special_handler(); }
. Dies ist schneller als ein Sprungtisch, insb. im nicht genommenen Fall.Ich bin mir bei Best Practices nicht sicher, aber ich würde den Schalter verwenden - und dann den absichtlichen Durchfall über "Standard" abfangen.
quelle
Ästhetisch tendiere ich dazu, diesen Ansatz zu bevorzugen.
Machen Sie die Daten etwas intelligenter, damit wir die Logik etwas dümmer machen können.
Mir ist klar, dass es komisch aussieht. Hier ist die Inspiration (wie ich es in Python machen würde):
quelle
break
externe acase
(ja, eine Moll -Operation) verwendet Sünde, aber dennoch eine Sünde). :)Wahrscheinlich wird die erste vom Compiler optimiert, was erklären würde, warum die zweite Schleife langsamer ist, wenn die Anzahl der Schleifen erhöht wird.
quelle
if
Vergleichswitch
zu einer Schleifenendbedingung in Java.Beim Kompilieren des Programms weiß ich nicht, ob es einen Unterschied gibt. Aber was das Programm selbst betrifft und den Code so einfach wie möglich zu halten, denke ich persönlich, dass es davon abhängt, was Sie tun möchten. if else if else Aussagen haben ihre Vorteile, die ich denke:
Mit dieser Funktion können Sie eine Variable anhand bestimmter Bereiche testen. Sie können Funktionen (Standard Library oder Personal) als Bedingungen verwenden.
(Beispiel:
Wenn sonst, wenn sonst, können Aussagen (trotz Ihrer besten Versuche) in Eile kompliziert und chaotisch werden. Switch-Anweisungen sind in der Regel klarer, sauberer und leichter zu lesen. kann aber nur zum Testen gegen bestimmte Werte verwendet werden (Beispiel:
Ich bevorzuge if - else if - else-Aussagen, aber es liegt wirklich an Ihnen. Wenn Sie Funktionen als Bedingungen verwenden möchten oder etwas gegen einen Bereich, ein Array oder einen Vektor testen möchten und / oder es Ihnen nichts ausmacht, sich mit der komplizierten Verschachtelung zu befassen, würde ich die Verwendung von If else if else-Blöcken empfehlen. Wenn Sie anhand einzelner Werte testen möchten oder einen sauberen und leicht lesbaren Block wünschen, würde ich die Verwendung von case-Blöcken (switch ()) empfehlen.
quelle
Ich bin nicht die Person, die Ihnen etwas über Geschwindigkeit und Speichernutzung erzählt, aber das Betrachten einer Switch-Anweisung ist verdammt viel einfacher zu verstehen als eine große if-Anweisung (insbesondere 2-3 Monate später).
quelle
Ich weiß, dass es alt ist, aber
Das Variieren der Schleifenzahl ändert sich stark:
Während if / else: 5ms Switch: 1ms Max Loops: 100000
Während if / else: 5ms Switch: 3ms Max Loops: 1000000
Während if / else: 5ms Switch: 14ms Max Loops: 10000000
Während if / else: 5ms Switch: 149ms Max Loops: 100000000
(Fügen Sie weitere Anweisungen hinzu, wenn Sie möchten)
quelle
if(max) break
Schleife läuft unabhängig von der Anzahl der Schleifen in konstanter Zeit? Klingt so, als ob der JIT-Compiler intelligent genug ist, um die Schleife zu optimierencounter2=max
. Und vielleicht ist es langsamer als das Umschalten, wenn der erste AufrufcurrentTimeMillis
mehr Overhead hat, weil noch nicht alles JIT-kompiliert ist? Wenn Sie die Schleifen in die andere Reihenfolge bringen, erhalten Sie wahrscheinlich andere Ergebnisse.