it-swarm.com.de

Vorteil der Umschaltung der if-else-Anweisung

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 ungefähr 10 eine erwartete Aktion haben (die derzeit dieselbe Aktion ist)? Leistung und Platzbedarf müssen berücksichtigt werden, sind jedoch nicht kritisch. Ich habe das Snippet abstrahiert, also hasse mich nicht für die Namenskonventionen.

switch Anweisung:

// 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 Anweisung:

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

Schalter benutzen.

Im schlimmsten Fall generiert der Compiler denselben Code wie eine If-else-Kette, sodass Sie nichts verlieren. Im Zweifelsfall stehen die häufigsten Fälle an erster Stelle in der switch-Anweisung.

Im besten Fall kann das Optimierungsprogramm einen besseren Weg finden, um den Code zu generieren. Üblicherweise erstellt ein Compiler einen binären Entscheidungsbaum (speichert Vergleiche und Sprünge im Durchschnitt) oder erstellt einfach eine Sprungtabelle (funktioniert überhaupt ohne Vergleiche).

152

Für den speziellen Fall, den Sie in Ihrem Beispiel angegeben haben, lautet der klarste Code wahrscheinlich:

if (RequiresSpecialEvent(numError))
    fire_special_event();

Dies verschiebt das Problem natürlich nur in einen anderen Bereich des Codes, aber jetzt haben Sie die Möglichkeit, diesen Test erneut zu verwenden. Sie haben auch mehr Möglichkeiten, wie Sie es lösen können. Sie könnten std :: set verwenden, zum Beispiel:

bool RequiresSpecialEvent(int numError)
{
    return specialSet.find(numError) != specialSet.end();
}

Ich behaupte nicht, dass dies die beste Implementierung von RequiresSpecialEvent ist, nur dass dies eine Option ist. Sie können immer noch eine Schalter- oder If-else-Kette, eine Nachschlagetabelle oder eine Bit-Manipulation für den Wert verwenden. Je unklarer Ihr Entscheidungsprozess wird, desto mehr Nutzen ziehen Sie daraus, wenn Sie ihn in einer isolierten Funktion haben.

42
Mark Ransom

Der Schalter ist schneller.

Probieren Sie einfach aus, ob sich 30 verschiedene Werte in einer Schleife befinden, und vergleichen Sie sie mit switch mit demselben Code, um festzustellen, wie viel schneller der Switch ist.

Jetzt hat der Schalter ein echtes Problem : Der Schalter muss zur Kompilierungszeit die Werte in jedem Fall kennen. Dies bedeutet, dass der folgende Code:

// WON'T COMPILE
extern const int MY_VALUE ;

void doSomething(const int p_iValue)
{
    switch(p_iValue)
    {
       case MY_VALUE : /* do something */ ; break ;
       default : /* do something else */ ; break ;
    }
}

wird nicht kompiliert.

Die meisten Benutzer verwenden dann define (Aargh!) Und andere deklarieren und definieren konstante Variablen in derselben Kompilierungseinheit. Beispielsweise:

// WILL COMPILE
const int MY_VALUE = 25 ;

void doSomething(const int p_iValue)
{
    switch(p_iValue)
    {
       case MY_VALUE : /* do something */ ; break ;
       default : /* do something else */ ; break ;
    }
}

Am Ende muss der Entwickler also zwischen "Geschwindigkeit + Klarheit" und "Codekopplung" wählen.

(Nicht, dass ein Schalter nicht als verdammt verwirrend beschrieben werden kann ... Die meisten Schalter, die ich derzeit sehe, gehören zu dieser "verwirrenden" Kategorie ... Aber das ist eine andere Geschichte ...)

Edit 2008-09-21:

bk1e hat folgenden Kommentar hinzugefügt: "Das Definieren von Konstanten als Aufzählungen in einer Header - Datei ist eine weitere Möglichkeit, dies zu handhaben ".

Natürlich ist es das.

Der Zweck eines externen Typs bestand darin, den Wert von der Quelle zu entkoppeln. Das Definieren dieses Werts als Makro, als einfache const int-Deklaration oder sogar als Aufzählung hat den Nebeneffekt, den Wert zu inlinieren. Sollte sich der Wert define, enum oder const int ändern, ist eine Neukompilierung erforderlich. Die externe Deklaration bedeutet, dass bei einer Wertänderung kein Neukompilieren erforderlich ist. Andererseits ist die Verwendung von switch nicht möglich. Die Schlussfolgerung ist Die Verwendung von switch erhöht die Kopplung zwischen dem Schaltercode und den als Fälle verwendeten Variablen. Wenn es ok ist, dann benutze switch. Wenn es nicht so ist, ist das keine Überraschung.

.

Bearbeiten Sie 2013-01-15:

Vlad Lazarenko hat meine Antwort kommentiert und einen Link zu seiner eingehenden Untersuchung des Assembler-Codes angegeben, der durch einen Schalter generiert wurde. Sehr aufschlussreich: http://lazarenko.me/switch/

19
paercebal

Der Compiler wird es trotzdem optimieren - wählen Sie den Schalter, da er am besten lesbar ist.

18

Der Schalter, wenn auch nur zur besseren Lesbarkeit. Sehr wichtig, wenn Aussagen meiner Meinung nach schwerer zu pflegen und zu lesen sind.

ERROR_01: // absichtliches Durchfallen

oder

(ERROR_01 == numError)

Letzteres ist fehleranfälliger und erfordert mehr Eingabe und Formatierung als das erste.

6
scubabbl

Code für die Lesbarkeit. Wenn Sie wissen möchten, welche Leistung besser ist, verwenden Sie einen Profiler, da Optimierungen und Compiler unterschiedlich sind und Leistungsprobleme selten dort auftreten, wo die Leute glauben, dass sie sind.

5
Bdoserror

Verwenden Sie switch, es ist das, wofür es ist und was Programmierer erwarten.

Ich würde die überflüssigen Falletiketten einfügen - nur um den Leuten das Gefühl zu geben, sich wohl zu fühlen, versuchte ich mich zu erinnern, wann/nach welchen Regeln sie ausgelassen wurden.
Sie möchten nicht, dass der nächste Programmierer, der daran arbeitet, unnötig über Sprachdetails nachdenkt (möglicherweise sind Sie es in ein paar Monaten!).

5
Martin Beckett

Compiler sind wirklich gut darin, switch zu optimieren. Der aktuelle gcc ist auch gut darin, eine Reihe von Bedingungen in einem if zu optimieren.

Ich habe einige Testfälle auf Godbolt gemacht.

Wenn die case -Werte nahe beieinander gruppiert sind, sind gcc, clang und icc intelligent genug, um mithilfe einer Bitmap zu überprüfen, ob es sich bei einem Wert um einen bestimmten handelt.

z.B. gcc 5.2 -O3 kompiliert das switch zu (und das if etwas sehr Ähnliches):

errhandler_switch(errtype):  # gcc 5.2 -O3
    cmpl    $32, %edi
    ja  .L5
    movabsq $4301325442, %rax   # highest set bit is bit 32 (the 33rd bit)
    btq %rdi, %rax
    jc  .L10
.L5:
    rep ret
.L10:
    jmp fire_special_event()

Beachten Sie, dass es sich bei der Bitmap um unmittelbare Daten handelt, sodass kein potenzieller Daten-Cache-Fehler beim Zugriff darauf oder eine Sprungtabelle vorliegt.

gcc 4.9.2 -O3 kompiliert das switch zu einer Bitmap, führt aber das 1U<<errNumber mit mov/shift. Es kompiliert die if -Version zu einer Reihe von Zweigen.

errhandler_switch(errtype):  # gcc 4.9.2 -O3
    leal    -1(%rdi), %ecx
    cmpl    $31, %ecx    # cmpl $32, %edi  wouldn't have to wait an extra cycle for lea's output.
              # However, register read ports are limited on pre-SnB Intel
    ja  .L5
    movl    $1, %eax
    salq    %cl, %rax   # with -march=haswell, it will use BMI's shlx to avoid moving the shift count into ecx
    testl   $2150662721, %eax
    jne .L10
.L5:
    rep ret
.L10:
    jmp fire_special_event()

Beachten Sie, wie 1 von errNumber subtrahiert wird (mit lea, um diese Operation mit einem Zug zu kombinieren). Auf diese Weise wird die Bitmap in eine 32-Bit-Direktverbindung eingepasst, wobei die 64-Bit-Direktverbindung movabsq vermieden wird, die mehr Befehlsbytes benötigt.

Eine kürzere (im Maschinencode) Sequenz wäre:

    cmpl    $32, %edi
    ja  .L5
    mov     $2150662721, %eax
    dec     %edi   # movabsq and btq is fewer instructions / fewer Intel uops, but this saves several bytes
    bt     %edi, %eax
    jc  fire_special_event
.L5:
    ret

(Die Nichtverwendung von jc fire_special_event ist allgegenwärtig und ein Compiler-Fehler .)

rep ret wird in Verzweigungszielen und folgenden bedingten Verzweigungen zum Nutzen der alten AMD K8 und K10 (vor 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 des Linksschiebens einer 1 um errNumber Bits und des Machens einer test, hat aber immer noch eine Latenz von 1 Zyklus und nur ein einziges Intel-UOP. Bei einem Speicherargument ist es aufgrund seiner viel zu CISC-Semantik langsam: Bei einem Speicheroperanden für die "Bitfolge" wird die Adresse des zu testenden Bytes basierend auf dem anderen Arg (geteilt durch 8) und isn berechnet Es ist nicht auf den 1, 2, 4 oder 8-Byte-Block beschränkt, auf den der Speicheroperand zeigt.

Ab Agner Fogs Anweisungstabellen ist eine Anweisung zur Verschiebung mit variabler Anzahl langsamer als eine bt auf neueren Intel-Versionen (2 Uops anstelle von 1, und die Verschiebung erledigt nicht alles, was benötigt wird) .

3
Peter Cordes

IMO ist dies ein perfektes Beispiel dafür, wofür Switch-Fall-Through gemacht wurde.

2
Nescio

Ich bin mit der Kompatibilität der Switch-Lösung einverstanden, aber IMO bist du entführt den Switch hier.
Der Zweck des Schalters ist es, je nach Wert nterschiedlich zu handhaben.
Wenn Sie Ihr Algo in Pseudocode erklären müssten, würden Sie ein if verwenden, weil das semantisch so ist: wenn whatever_error dies tut ...
Wenn Sie also nicht beabsichtigen, Ihren Code eines Tages so zu ändern, dass er für jeden Fehler einen bestimmten Code enthält, würde ich if verwenden.

1
François
while (true) != while (loop)

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.

1
MarioFrost

Ästhetisch neige ich dazu, diesen Ansatz zu bevorzugen.

unsigned int special_events[] = {
    ERROR_01,
    ERROR_07,
    ERROR_0A,
    ERROR_10,
    ERROR_15,
    ERROR_16,
    ERROR_20
 };
 int special_events_length = sizeof (special_events) / sizeof (unsigned int);

 void process_event(unsigned int numError) {
     for (int i = 0; i < special_events_length; i++) {
         if (numError == special_events[i]) {
             fire_special_event();
             break;
          }
     }
  }

Machen Sie die Daten ein bisschen schlauer, damit die Logik ein bisschen langweiliger wird.

Mir ist klar, dass es komisch aussieht. Hier ist die Inspiration (wie ich es in Python gemacht habe):

special_events = [
    ERROR_01,
    ERROR_07,
    ERROR_0A,
    ERROR_10,
    ERROR_15,
    ERROR_16,
    ERROR_20,
    ]
def process_event(numError):
    if numError in special_events:
         fire_special_event()
1
mbac32768

Ich bin mir nicht sicher, was Best Practices angeht, aber ich würde switch verwenden - und dann absichtliches Durchfallen über "default" abfangen.

1
da5id

Wenn Ihre Fälle wahrscheinlich in Zukunft gruppiert bleiben - wenn mehr als ein Fall einem Ergebnis entspricht - kann sich herausstellen, dass der Schalter leichter zu lesen und zu warten ist.

1
TSomKes

Sie funktionieren genauso gut. Die Leistung ist bei einem modernen Compiler ungefähr gleich.

Ich bevorzuge if-Anweisungen gegenüber case-Anweisungen, da sie besser lesbar 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, nur das zu tun, was für Sie am besten lesbar ist.

1
SquareCog

schalter wird definitiv bevorzugt. Es ist einfacher, sich die Liste der Fälle eines Schalters anzusehen und sicher zu wissen, was er tut, als die lange if-Bedingung zu lesen.

Die Vervielfältigung im Zustand if ist schwer für die Augen. Angenommen, einer der == wurde geschrieben !=; würdest du es merken Oder wenn eine Instanz von 'numError' 'nmuError' geschrieben wurde, was passierte gerade zu kompilieren?

Ich bevorzuge im Allgemeinen die Verwendung von Polymorphismus anstelle des Schalters, aber ohne nähere Angaben zum Kontext ist es schwer zu sagen.

Was die Leistung angeht, ist es am besten, einen Profiler zu verwenden, um die Leistung Ihrer Anwendung unter Bedingungen zu messen, die denen ähneln, die Sie in der Natur erwarten. Andernfalls optimieren Sie wahrscheinlich am falschen Ort und auf die falsche Weise.

1
Jay Bazuzi

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. Wenn Sie die Änderung nur explizit implementieren müssen, 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.

switch(numerror){
    ERROR_20 : { fire_special_event(); } break;
    default : { null; } break;
}

Vielleicht testen Sie sogar Ihren Zustand (in diesem Fall numerror) anhand einer Liste von Möglichkeiten, vielleicht eines Arrays, damit Ihr SWITCH nicht einmal verwendet wird, es sei denn, es gibt definitiv ein Ergebnis.

0
lewis

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. Dadurch wird der Code auch 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 anstoßen.

0
Greg Whitfield

Wenn es darum geht, das Programm zu kompilieren, weiß ich nicht, ob es einen Unterschied gibt. Aber was das Programm selbst betrifft und den Code so einfach wie möglich hält, denke ich persönlich, dass es davon abhängt, was Sie tun möchten. wenn sonst wenn sonst aussagen haben ihre vorteile, die ich denke sind:

mit dieser Option können Sie eine Variable gegen bestimmte Bereiche testen, für die Sie Funktionen (Standardbibliothek oder Persönlich) als Bedingungen verwenden können.

(Beispiel:

`int a;
 cout<<"enter value:\n";
 cin>>a;

 if( a > 0 && a < 5)
   {
     cout<<"a is between 0, 5\n";

   }else if(a > 5 && a < 10)

     cout<<"a is between 5,10\n";

   }else{

       "a is not an integer, or is not in range 0,10\n";

Allerdings, wenn sonst, wenn sonst Aussagen können kompliziert und chaotisch (trotz Ihrer besten Versuche) in Eile. Switch-Anweisungen sind in der Regel klarer, sauberer und leichter zu lesen. kann aber nur zum Testen gegen bestimmte Werte verwendet werden (Beispiel:

`int a;
 cout<<"enter value:\n";
 cin>>a;

 switch(a)
 {
    case 0:
    case 1:
    case 2: 
    case 3:
    case 4:
    case 5:
        cout<<"a is between 0,5 and equals: "<<a<<"\n";
        break;
    //other case statements
    default:
        cout<<"a is not between the range or is not a good value\n"
        break;

Ich bevorzuge if - else if - else Aussagen, aber es liegt wirklich an Ihnen. Wenn Sie Funktionen als Bedingungen verwenden oder etwas gegen einen Bereich, ein Array oder einen Vektor testen möchten und/oder sich nicht mit der komplizierten Verschachtelung befassen möchten, würde ich die Verwendung von If else if else blocks empfehlen. Wenn Sie gegen einzelne Werte testen möchten oder einen sauberen und einfach zu lesenden Block möchten, würde ich die Verwendung von switch () - Case-Blöcken empfehlen.

0
Jordan Effinger

Ich weiß, dass es alt ist, aber

public class SwitchTest {
static final int max = 100000;

public static void main(String[] args) {

int counter1 = 0;
long start1 = 0l;
long total1 = 0l;

int counter2 = 0;
long start2 = 0l;
long total2 = 0l;
boolean loop = true;

start1 = System.currentTimeMillis();
while (true) {
  if (counter1 == max) {
    break;
  } else {
    counter1++;
  }
}
total1 = System.currentTimeMillis() - start1;

start2 = System.currentTimeMillis();
while (loop) {
  switch (counter2) {
    case max:
      loop = false;
      break;
    default:
      counter2++;
  }
}
total2 = System.currentTimeMillis() - start2;

System.out.println("While if/else: " + total1 + "ms");
System.out.println("Switch: " + total2 + "ms");
System.out.println("Max Loops: " + max);

System.exit(0);
}
}

Die Anzahl der Schleifen ä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)

0
McAnix

Ich würde die if-Anweisung aus Gründen der Klarheit und Konvention auswählen, obwohl ich mir sicher bin, dass einige nicht zustimmen würden. Schließlich möchten Sie etwas tun if eine Bedingung ist wahr! Einen Schalter mit einer Aktion zu haben, scheint ein wenig ... unnötig.

0
William Keller

Bitte benutzen Sie den Schalter. Die if-Anweisung benötigt eine der Anzahl der Bedingungen proportionale Zeit.

0
David Nehme

Ich bin nicht die Person, die Ihnen etwas über Geschwindigkeit und Speichernutzung sagt, aber das Betrachten einer switch-Anweisung ist verdammt viel einfacher zu verstehen als eine große if-Anweisung (insbesondere 2-3 Monate später).

0
Ed Brown