Domanda

Qual è la procedura migliore per utilizzare a switch istruzione rispetto all'utilizzo di un if dichiarazione per 30 unsigned enumerazioni in cui circa 10 hanno un'azione prevista (che attualmente è la stessa azione).Le prestazioni e lo spazio devono essere considerati ma non sono fondamentali.Ho estratto lo snippet quindi non odiarmi per le convenzioni di denominazione.

switch dichiarazione:

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

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();
}
È stato utile?

Soluzione

Usa l'interruttore.

Nel peggiore dei casi il compilatore genererà lo stesso codice di una catena if-else, quindi non perdi nulla.In caso di dubbi, inserire prima i casi più comuni nell'istruzione switch.

Nel migliore dei casi l'ottimizzatore potrebbe trovare un modo migliore per generare il codice.Le cose comuni che fa un compilatore è costruire un albero decisionale binario (salva confronti e salti nel caso medio) o semplicemente costruire una tabella di salto (funziona senza confronti).

Altri suggerimenti

Per il caso speciale che hai fornito nel tuo esempio, il codice più chiaro è probabilmente:

if (RequiresSpecialEvent(numError))
    fire_special_event();

Ovviamente questo sposta semplicemente il problema in un'area diversa del codice, ma ora hai l'opportunità di riutilizzare questo test.Hai anche più opzioni su come risolverlo.Potresti usare std::set, ad esempio:

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

Non sto suggerendo che questa sia la migliore implementazione di RequiresSpecialEvent, solo che è un'opzione.Puoi comunque utilizzare un interruttore o una catena if-else, o una tabella di ricerca, o qualche manipolazione di bit sul valore, qualunque cosa.Più oscuro diventa il tuo processo decisionale, maggiore sarà il valore che trarrai dall'averlo in una funzione isolata.

L'interruttore È Più veloce.

Prova semplicemente if/else a inserire 30 valori diversi all'interno di un ciclo e confrontalo con lo stesso codice utilizzando switch per vedere quanto è più veloce lo switch.

Ora il l'interruttore ha un vero problema :Lo switch deve conoscere in fase di compilazione i valori all'interno di ciascun caso.Ciò significa che il seguente codice:

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

non verrà compilato.

La maggior parte delle persone utilizzerà quindi le definizioni (Aargh!) e altri dichiareranno e definiranno variabili costanti nella stessa unità di compilazione.Per esempio:

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

Quindi, alla fine, lo sviluppatore deve scegliere tra "velocità + chiarezza" vs."accoppiamento di codici".

(Non che un interruttore non possa essere scritto in modo da creare confusione da morire...La maggior parte degli interruttori che vedo attualmente appartengono a questa categoria "confusa""...Ma questa è un'altra storia...)

Modifica 21-09-2008:

bk1e ha aggiunto il seguente commento:"Definire le costanti come enumerazioni in un file di intestazione è un altro modo per gestire questa situazione".

Ovviamente è.

Lo scopo di un tipo extern era disaccoppiare il valore dall'origine.Definire questo valore come una macro, come una semplice dichiarazione const int o anche come un'enumerazione ha l'effetto collaterale di incorporare il valore.Pertanto, se il valore define, enum o const int dovesse cambiare, sarebbe necessaria una ricompilazione.La dichiarazione extern significa che non è necessario ricompilare in caso di modifica del valore, ma d'altra parte rende impossibile l'uso di switch.La conclusione è L'utilizzo di switch aumenterà l'accoppiamento tra il codice switch e le variabili utilizzate come casi.Quando è ok, usa l'interruttore.Quando non lo è, allora, nessuna sorpresa.

.

Modifica 15-01-2013:

Vlad Lazarenko ha commentato la mia risposta, fornendo un link al suo studio approfondito del codice assembly generato da uno switch.Molto illuminante: http://741mhz.com/switch/

Il compilatore lo ottimizzerà comunque: scegli l'interruttore poiché è il più leggibile.

Lo Switch, anche solo per la leggibilità.Secondo me, le affermazioni if ​​giganti sono più difficili da mantenere e più difficili da leggere.

ERRORE_01 :// caduta intenzionale

O

(ERROR_01 == numErrore) ||

Il secondo è più soggetto a errori e richiede più digitazione e formattazione rispetto al primo.

Codice per la leggibilità.Se vuoi sapere cosa funziona meglio, usa un profiler, poiché le ottimizzazioni e i compilatori variano e i problemi di prestazioni raramente sono dove le persone pensano che siano.

Usa switch, è a cosa serve e cosa si aspettano i programmatori.

Tuttavia inserirei le etichette ridondanti dei casi: solo per far sentire le persone a proprio agio, stavo cercando di ricordare quando / quali sono le regole per lasciarle fuori.
Non vuoi che il prossimo programmatore che ci lavorerà debba pensare inutilmente ai dettagli della lingua (potresti essere tu tra qualche mese!)

I compilatori sono davvero bravi a ottimizzare switch.Il recente gcc è anche bravo a ottimizzare una serie di condizioni in un file if.

Ho realizzato alcuni casi di test su godbolt.

Quando il case i valori sono raggruppati vicini, gcc, clang e icc sono tutti abbastanza intelligenti da utilizzare una bitmap per verificare se un valore è uno di quelli speciali.

per esempio.gcc 5.2 -O3 compila il file switch a (e il if qualcosa di molto simile):

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

Si noti che la bitmap è un dato immediato, quindi non vi è alcun potenziale errore nella cache dei dati durante l'accesso o una tabella di salto.

gcc 4.9.2 -O3 compila il switch a una bitmap, ma fa il 1U<<errNumber con mov/shift.Compila il if versione a serie di rami.

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

Nota come sottrae 1 da errNumber (con lea per combinare l'operazione con uno spostamento).Ciò gli consente di adattare la bitmap a un immediato a 32 bit, evitando l'immediato a 64 bit movabsq che richiede più byte di istruzioni.

Una sequenza più breve (in codice macchina) sarebbe:

    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

(Il mancato utilizzo jc fire_special_event è onnipresente, ed è un bug del compilatore.)

rep ret viene utilizzato nei branch target e nei seguenti branch condizionali, a vantaggio dei vecchi AMD K8 e K10 (pre-Bulldozer): Cosa significa "rep ret"?.Senza di essa, la previsione dei rami non funziona altrettanto bene su quelle CPU obsolete.

bt (bit test) con un registro arg è veloce.Combina il lavoro di spostamento a sinistra di 1 per errNumber bit e fare a test, ma la latenza è ancora di 1 ciclo e solo un singolo Intel uop.È lento con un argomento di memoria a causa della sua semantica troppo CISC:con un operando di memoria per la "stringa di bit", l'indirizzo del byte da testare viene calcolato in base all'altro argomento (diviso per 8) e non è limitato al blocco da 1, 2, 4 o 8 byte puntato dall'operando di memoria.

Da Le tabelle di istruzioni di Agner Fog, un'istruzione di spostamento a conteggio variabile è più lenta di a bt sui recenti Intel (2 uops invece di 1 e shift non fa tutto il resto necessario).

IMO questo è un perfetto esempio di ciò per cui è stato creato il fall-through.

Se è probabile che i tuoi casi rimangano raggruppati in futuro, ovvero se più di un caso corrisponde a un risultato, il passaggio potrebbe rivelarsi più semplice da leggere e gestire.

Funzionano altrettanto bene.Le prestazioni sono più o meno le stesse con un compilatore moderno.

Preferisco le istruzioni if ​​rispetto alle istruzioni case perché sono più leggibili e più flessibili: puoi aggiungere altre condizioni non basate sull'uguaglianza numerica, come " || max < min ".Ma per il semplice caso che hai pubblicato qui, non ha molta importanza, fai solo ciò che è più leggibile per te.

il cambio è decisamente preferibile.È più semplice consultare l'elenco dei casi di uno switch e sapere con certezza cosa sta facendo piuttosto che leggere la condizione if lunga.

La duplicazione in if la condizione è dura per gli occhi.Supponiamo che uno dei == fu scritto !=;te ne accorgeresti?O se un'istanza di "numError" fosse scritta "nmuError", che è appena stata compilata?

In genere preferirei utilizzare il polimorfismo anziché l'interruttore, ma senza ulteriori dettagli sul contesto è difficile dirlo.

Per quanto riguarda le prestazioni, la soluzione migliore è utilizzare un profiler per misurare le prestazioni della tua applicazione in condizioni simili a quelle che ti aspetteresti in natura.Altrimenti probabilmente stai ottimizzando nel posto sbagliato e nel modo sbagliato.

Sono d'accordo con la compattezza della soluzione switch ma IMO lo sei dirottare l'interruttore Qui.
Lo scopo dell'interruttore è avere diverso manipolazione a seconda del valore.
Se dovessi spiegare il tuo algoritmo in pseudo-codice, useresti un if perché, semanticamente, è quello che è: se qualunque_errore fai questo...
Quindi, a meno che un giorno non intendi modificare il codice per avere un codice specifico per ogni errore, utilizzerei Se.

Non sono sicuro delle migliori pratiche, ma utilizzerei lo switch e quindi intrappolerei il fall-through intenzionale tramite "default"

Esteticamente tendo a favorire questo approccio.

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

Rendere i dati un po' più intelligenti così da poter rendere la logica un po' più stupida.

Mi rendo conto che sembra strano.Ecco l'ispirazione (da come lo farei in Python):

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)

Probabilmente il primo è ottimizzato dal compilatore, il che spiegherebbe perché il secondo ciclo è più lento quando si aumenta il numero di cicli.

Sceglierei l'istruzione if per ragioni di chiarezza e convenzione, anche se sono sicuro che alcuni non sarebbero d'accordo.Dopotutto, vuoi fare qualcosa if alcune condizioni sono vere!Avere un cambio con una sola azione sembra un po'...inutile.

Non sono la persona che ti parla della velocità e dell'utilizzo della memoria, ma guardare un'istruzione switch è molto più facile da capire rispetto a un'istruzione if di grandi dimensioni (specialmente 2-3 mesi dopo)

Direi di usare SWITCH.In questo modo devi solo implementare risultati diversi.I tuoi dieci casi identici possono utilizzare l'impostazione predefinita.In caso di modifica, è sufficiente implementare esplicitamente la modifica, non è necessario modificare l'impostazione predefinita.È anche molto più semplice aggiungere o rimuovere casi da uno SWITCH piuttosto che modificare IF e ELSEIF.

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

Forse anche testare la tua condizione (in questo caso numerror) rispetto a un elenco di possibilità, un array forse in modo che il tuo SWITCH non venga nemmeno utilizzato a meno che non ci sia sicuramente un risultato.

Visto che hai solo 30 codici di errore, codifica la tua tabella di salto, quindi fai tu stesso tutte le scelte di ottimizzazione (il salto sarà sempre il più veloce), piuttosto che sperare che il compilatore faccia la cosa giusta.Inoltre rende il codice molto piccolo (a parte la dichiarazione statica della tabella di salto).Ha anche il vantaggio collaterale che con un debugger puoi modificare il comportamento in fase di esecuzione se necessario, semplicemente inserendo direttamente i dati della tabella.

So che è vecchio ma

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

Variando il conteggio dei loop cambia molto:

Mentre if/else:Switch 5ms:1ms max loops:100000

Mentre if/else:Switch 5ms:3ms max loops:1000000

Mentre if/else:Switch 5ms:14ms max loops:10000000

Mentre if/else:Switch 5ms:149 ms max loop:100000000

(aggiungi altre affermazioni se vuoi)

Quando si tratta di compilare il programma, non so se c'è qualche differenza.Ma per quanto riguarda il programma in sé e il mantenimento del codice il più semplice possibile, personalmente penso che dipenda da cosa vuoi fare.if else if else le affermazioni hanno i loro vantaggi, che penso siano:

Consentire di testare una variabile contro intervalli specifici È possibile utilizzare le funzioni (libreria standard o personale) come condizionati.

(esempio:

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

Tuttavia, le istruzioni If else if else possono diventare complicate e disordinate (nonostante i tuoi migliori tentativi) in fretta.Le dichiarazioni di cambio tendono ad essere più chiare, più pulite e più facili da leggere;ma può essere utilizzato solo per verificare valori specifici (esempio:

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

Preferisco le affermazioni if ​​- else if - else, ma dipende davvero da te.Se vuoi utilizzare le funzioni come condizioni o vuoi testare qualcosa rispetto a un intervallo, array o vettore e/o non ti dispiace affrontare la complicata nidificazione, ti consiglio di utilizzare i blocchi If else if else.Se vuoi testare valori singoli o desideri un blocco pulito e facile da leggere, ti consiglio di utilizzare i blocchi case switch().

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top