Frage

Was ist die beste Praxis für die Verwendung von a switch Anweisung vs mit einem if Erklärung für 30 unsigned Aufzählungen, bei denen ungefähr 10 eine erwartete Maßnahme haben (die derzeit dieselbe Maßnahme ist). Leistung und Raum müssen berücksichtigt werden, sind aber nicht kritisch. Ich habe den Ausschnitt 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();
}
War es hilfreich?

Lösung

Switch verwenden.

Im schlimmsten Fall generiert der Compiler den gleichen Code wie eine IF-ELSE-Kette, sodass Sie nichts verlieren. Im Zweifelsfall die häufigsten Fälle zuerst in die Switch -Anweisung einfügen.

Im besten Fall kann der Optimierer einen besseren Weg finden, um den Code zu generieren. Gemeinsame Dinge, die ein Compiler tut, besteht darin, einen binären Entscheidungsbaum zu bauen (Paradenvergleiche und Sprünge in dem durchschnittlichen Fall) oder einfach einen Sprungtisch (funktioniert überhaupt ohne Vergleich).

Andere Tipps

Für den Sonderfall, den Sie in Ihrem Beispiel angegeben haben, ist der klarste Code wahrscheinlich:

if (RequiresSpecialEvent(numError))
    fire_special_event();

Offensichtlich verschiebt dies nur das Problem in einen anderen Bereich des Codes, aber jetzt haben Sie die Möglichkeit, diesen Test wiederzuverwenden. Sie haben auch mehr Optionen, um es zu lösen. Sie könnten STD :: SET verwenden, zum Beispiel:

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

Ich schlage nicht vor, dass dies die beste Implementierung der Anforderung ist, nur dass es sich um eine Option handelt. Sie können weiterhin einen Schalter oder eine IF-ELSE-Kette oder eine Nachschlagtabelle oder eine Bit-Manipulation des Wertes verwenden, was auch immer. Je dunkler Ihr Entscheidungsprozess wird, desto mehr Wert ergeben Sie ihn in einer isolierten Funktion.

Der Schalter ist Schneller.

Versuchen Sie es einfach, wenn/sonst 30 verschiedene Werte in einer Schleife mit demselben Code mit Switch vergleichen, um zu sehen, wie viel schneller der Switch ist.

Jetzt die Switch hat ein echtes Problem : Der Schalter muss zum Kompilieren der Werte in jedem Fall wissen. 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 kompilieren.

Die meisten Menschen werden dann Defines verwenden (AARGH!), Und andere deklarieren und definieren konstante Variablen in derselben Kompilierungseinheit. Zum Beispiel:

// 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 + Clarity" gegen "Code -Kopplung" wählen.

(Nicht, dass ein Switch nicht so geschrieben werden kann, dass sie höllisch verwirrend sind ... die meisten der Switch, den ich derzeit sehe, sind von dieser "verwirrenden" Kategorie "... aber das ist eine andere Geschichte ...)

Bearbeiten 2008-09-21:

Bk1e Der folgende Kommentar fügte hinzu: "Das Definieren von Konstanten in einer Header -Datei ist eine weitere Möglichkeit, dies zu verarbeiten.

Natürlich ist es das.

Der Punkt eines externen Typs bestand darin, den Wert aus der Quelle zu entkoppeln. Definieren dieses Wertes als Makro, als einfache Const Int-Deklaration oder sogar als Aufzählung hat die Nebenwirkung, den Wert zu beeinträchtigen. Sollte der Definieren, den Umlaufwert oder die Veränderung des Const -Int -Werts eine Neukompilation erforderlich sein. Die externe Deklaration bedeutet, dass es bei Wertänderungen nicht neu kompiliert werden muss, aber es macht es andererseits unmöglich, Switch zu verwenden. Das Schluss Die Verwendung von Switch erhöht die Kopplung zwischen dem Switch -Code und den als Fällen verwendeten Variablen. Wenn es in Ordnung ist, dann verwenden Sie Switch. Wenn es nicht ist, überraschen Sie es nicht.

.

Bearbeiten 2013-01-15:

Vlad Lazarenko kommentierte meine Antwort und gab einen Link zu seiner eingehenden Studie des Assembly-Code, der von einem Switch generiert wurde. Sehr erleuchtet: http://741mhz.com/switch/

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

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

Fehler_01 : // absichtliche Herbst-Through

oder

(ERROR_01 == Numerror) ||

Der spätere ist fehleranfälliger und erfordert mehr Tippen und Formatierung als die erste.

Code für Lesbarkeit. Wenn Sie wissen möchten, was besser abschneidet, verwenden Sie einen Profiler, da Optimierungen und Compiler variieren, und Leistungsprobleme sind selten dort, wo die Leute sie sind.

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

Ich würde die überflüssigen Fallbezeichnungen jedoch einfügen - nur um die Leute sich wohl zu fühlen, versuchte ich mich daran zu erinnern, wann / wie die Regeln sie auslassen.
Sie möchten nicht, dass der nächste Programmierer daran arbeitet, unnötig über Sprachdetails nachzudenken (Sie könnten in ein paar Monaten sein!)

Compiler sind wirklich gut zu optimieren switch. Die jüngste GCC ist auch gut darin, eine Reihe von Bedingungen in einem zu optimieren if.

Ich habe einige Testfälle aufgenommen Godbolt.

Wenn der case Die Werte werden nahe beieinander gruppiert, GCC, Clang und ICC sind alle klug genug, um eine Bitmap zu verwenden, um zu überprüfen, ob ein Wert einer der besonderen ist.

zB GCC 5.2 -O3 kompiliert die switch zu (und die 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. Daher gibt es keinen potenziellen Datencache-Miss, der auf sie zugriff oder eine Sprungtabelle.

GCC 4.9.2 -O3 kompiliert die switch zu einer Bitmap, aber die 1U<<errNumber mit MOV/Shift. Es kompiliert die if Version zu Serien 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 es 1 von abzieht errNumber (mit lea um diesen Betrieb mit einem Umzug zu kombinieren). Das ermöglicht es, die Bitmap in einen 32-Bit-Sofort zu passen, um das 64-Bit-Immediat zu vermeiden movabsq Das erfordert mehr Anweisungsbytes.

Eine kürzere (in 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

(Das Versäumnis zu verwenden jc fire_special_event ist allgegenwärtig und ist Ein Compiler -Fehler.)

rep ret wird in Zweigzielen und nach bedingten Zweigen zum Nutzen des alten AMD K8 und K10 (Pre-Bulldozer) verwendet: Was bedeutet "Rep Ret"?. Ohne sie funktioniert die Vorhersage der Zweig bei diesen veralteten CPUs nicht so gut.

bt (Bit -Test) mit einem Register Arg ist schnell. Es kombiniert die Arbeit, eine 1 von links zu verschieben errNumber Teile und a test, ist aber immer noch 1 Zykluslatenz und nur ein einzelner Intel UOP. Mit einem Speicher-Arg-Arg-Semantik ist es langsam: Mit einem Speicheroperanden für die "Bit-String" wird die Adresse des zu testen 'T begrenzt auf den 1, 2, 4 oder 8Byte -Chunk, auf den der Speicheroperand hingewiesen wurde.

Aus Anweisungstische von Agner Fog, eine Schichtanweisung mit variabler Zählern ist langsamer als a bt Bei jüngsten Intel (2 UOPS anstelle von 1 und Shift tun nicht alles andere, was benötigt wird).

IMO Dies ist ein perfektes Beispiel dafür, wofür der Schalter Herbst gemacht wurde.

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

Sie funktionieren gleich gut. Die Leistung ist ungefähr gleich angesichts eines modernen Compilers.

Ich bevorzuge, ob Aussagen über Fallanweisungen, weil sie lesbarer und flexibler sind - Sie können andere Bedingungen hinzufügen, die nicht auf der numerischen Gleichheit basieren, wie "|| max <min". Aber für den einfachen Fall, den Sie hier gepostet haben, spielt es keine Rolle, dass Sie einfach das tun können, was Ihnen am lesbarsten ist.

Switch ist definitiv bevorzugt. Es ist einfacher, sich die Fälle eines Switchs zu untersuchen und sicher zu wissen, was es tut, als die lange, wenn auch Bedingung zu lesen.

Die Duplizierung in der if Der Zustand ist schwer für die Augen. Angenommen, einer der == wurde geschrieben !=; Würden Sie es bemerken? Oder wenn eine Instanz von 'Numerror' 'nmuError' geschrieben wurde, was gerade kompiliert wurde?

Ich würde im Allgemeinen lieber Polymorphismus anstelle des Switchs verwenden, aber ohne weitere Details des Kontexts ist es schwer zu sagen.

Was die Leistung betrifft, ist es am besten, einen Profiler zu verwenden, um die Leistung Ihrer Anwendung unter Bedingungen zu messen, die dem ähnlich sind, was Sie in freier Wildbahn erwarten. Andernfalls optimieren Sie wahrscheinlich auf falscher Ort und auf die falsche Weise.

Ich stimme der Komposition der Switch -Lösung zu, aber imo Sie sind Sie Entführung des Schalters hier.
Der Zweck des Schalters ist zu haben anders Handhabung abhängig vom Wert.
Wenn Sie Ihr Algo in Pseudo-Code erklären müssten, würden Sie ein if verwenden, weil es semantisch das ist, was es ist: Wenn was auch immer....
Wenn Sie also nicht eines Tages beabsichtigen, Ihren Code so zu ändern, dass sie für jeden Fehler einen bestimmten Code haben, würde ich verwenden wenn.

Ich bin mir nicht sicher über Best-Practise, aber ich würde Switch verwenden-und dann absichtliche Herbst durch "Standard" verangene

Ä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 etwas schlauer, damit wir die Logik ein wenig dümmer machen können.

Mir ist klar, dass es komisch aussieht. Hier ist die Inspiration (wie ich es in Python machen würde):

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()
while (true) != while (loop)

Wahrscheinlich wird der erste vom Compiler optimiert. Dies würde erklären, warum die zweite Schleife bei der Erhöhung der Schleifenzahl langsamer ist.

Ich würde die If -Aussage aussuchen, um Klarheit und Konvention zu gewährleisten, obwohl ich sicher bin, dass einige nicht zustimmen würden. Immerhin möchten Sie etwas tun if Ein Zustand ist wahr! Ein Wechsel mit einer Aktion zu haben, scheint ein wenig ... unnötig zu sein.

Ich bin nicht die Person, die Ihnen über Geschwindigkeits- und Speichernutzung erzählt, aber wenn Sie sich eine Switch-Aussage ansehen, ist es verdammt viel einfacher zu verstehen als eine große, identische Aussage (insbesondere 2-3 Monate später).

Ich würde sagen, verwenden Sie Switch. Auf diese Weise müssen Sie nur unterschiedliche Ergebnisse implementieren. Ihre zehn identischen Fälle können die Standardeinstellung verwenden. Sollte eine Änderung alles, was Sie brauchen, ist explizit die Änderung implementieren, ohne den Standard zu bearbeiten. Es ist auch viel einfacher, Fälle aus einem Switch hinzuzufügen oder zu entfernen, als zu bearbeiten, wenn und sonst.

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

Vielleicht testen Sie sogar Ihren Zustand (in diesem Fall Numerror) gegen eine Liste von Möglichkeiten, ein Array, sodass Ihr Switch nur verwendet wird, wenn es auf jeden Fall ein Ergebnis gibt.

Da Sie nur 30 Fehlercodes haben, codieren Sie Ihre eigene Sprungtabelle, dann treffen Sie alle Optimierungsentscheidungen selbst (Sprung wird immer am schnellsten sein), anstatt zu hoffen, dass der Compiler das Richtige tut. Es macht den Code auch sehr klein (abgesehen von der statischen Erklärung des Sprungtisches). Es hat auch den Nebennutzen, dass Sie mit einem Debugger das Verhalten zur Laufzeit ändern können, wenn Sie dies benötigen, nur indem Sie die Tabellendaten direkt stupsen.

Ich kenne es aber alt, 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 Variation der Schleifenzahl ändert sich stark:

Während if/else: 5ms Switch: 1 ms Max Loops: 100000

Während if/else: 5ms Switch: 3 ms Max Loops: 1000000

Während if/else: 5ms Switch: 14 ms Max Loops: 10000000

Während if/else: 5ms Switch: 149 ms Max Loops: 100000000

(Fügen Sie weitere Aussagen hinzu, wenn Sie möchten)

Wenn es darum geht, das Programm zusammenzustellen, 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 Anweisungen ihre Vorteile haben, die meiner Meinung nach sind:

Ermöglichen Sie, eine Variable gegen bestimmte Bereiche zu testen. Sie können Funktionen (Standardbibliothek oder persönliche) als Bedingungen verwenden.

(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";

Wenn jedoch sonst Anweisungen in Eile kompliziert und unordentlich werden können (trotz Ihrer besten Versuche). Switch -Anweisungen sind in der Regel klarer, sauberer und leichter zu lesen. kann aber nur verwendet werden, um gegen bestimmte Werte zu testen (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, wenn - sonst - sonst - sonst 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 Verschachtung zu befassen, würde ich empfehlen, wenn sonst sonst sonst noch Blöcke. Wenn Sie gegen einzelne Werte testen möchten oder einen sauberen und einfach zu lesenden Block wünschen, empfehlen ich, dass Sie Switch () -Kofferblöcke verwenden.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top