Pregunta

¿Cuál es la mejor práctica para usar un switch declaración versus usar una if declaración por 30 unsigned enumeraciones donde alrededor de 10 tienen una acción esperada (que actualmente es la misma acción).Es necesario considerar el rendimiento y el espacio, pero no son críticos.He resumido el fragmento, así que no me odiéis por las convenciones de nomenclatura.

switch declaración:

// 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 declaración:

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();
}
¿Fue útil?

Solución

Utilice el interruptor.

En el peor de los casos, el compilador generará el mismo código que una cadena if-else, para que no pierdas nada.En caso de duda, coloque primero los casos más comunes en la declaración de cambio.

En el mejor de los casos, el optimizador puede encontrar una mejor manera de generar el código.Lo común que hace un compilador es construir un árbol de decisión binario (guarda comparaciones y salta en el caso promedio) o simplemente construir una tabla de salto (funciona sin ninguna comparación).

Otros consejos

Para el caso especial que proporcionó en su ejemplo, el código más claro probablemente sea:

if (RequiresSpecialEvent(numError))
    fire_special_event();

Obviamente, esto simplemente traslada el problema a un área diferente del código, pero ahora tienes la oportunidad de reutilizar esta prueba.También tienes más opciones sobre cómo solucionarlo.Podrías usar std::set, por ejemplo:

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

No estoy sugiriendo que esta sea la mejor implementación de RequiresSpecialEvent, solo que es una opción.Todavía puedes usar un interruptor o una cadena if-else, o una tabla de búsqueda, o alguna manipulación de bits en el valor, lo que sea.Cuanto más oscuro se vuelva su proceso de decisión, más valor obtendrá al tenerlo en una función aislada.

El interruptor es más rápido.

Simplemente intente ingresar 30 valores diferentes dentro de un bucle y compárelo con el mismo código usando switch para ver cuánto más rápido es el cambio.

Ahora el El interruptor tiene un problema real. :El conmutador debe conocer en el momento de la compilación los valores dentro de cada caso.Esto significa que el siguiente código:

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

no se compilará.

La mayoría de la gente usará definiciones (¡Aargh!), y otros declararán y definirán variables constantes en la misma unidad de compilación.Por ejemplo:

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

Entonces, al final, el desarrollador debe elegir entre "velocidad + claridad" vs."acoplamiento de código".

(No es que no se pueda escribir un cambio para que sea muy confuso...La mayoría de los cambios que veo actualmente son de esta categoría "confusa"...Pero esta es otra historia...)

Editar 2008-09-21:

bk1e añadió el siguiente comentario:"Definir constantes como enumeraciones en un archivo de encabezado es otra forma de manejar esto".

Por supuesto que es.

El objetivo de un tipo externo era desacoplar el valor de la fuente.Definir este valor como una macro, como una simple declaración const int o incluso como una enumeración tiene el efecto secundario de incluir el valor.Por lo tanto, si cambia la definición, el valor de enumeración o el valor de const int, sería necesaria una recompilación.La declaración externa significa que no hay necesidad de volver a compilar en caso de cambio de valor, pero, por otro lado, hace imposible usar switch.La conclusión es El uso del interruptor aumentará el acoplamiento entre el código del interruptor y las variables utilizadas como casos..Cuando esté bien, utilice el interruptor.Cuando no lo es, entonces no es ninguna sorpresa.

.

Editar 2013-01-15:

Vlad Lázarenko comentó mi respuesta, dando un enlace a su estudio en profundidad del código ensamblador generado por un interruptor.Muy esclarecedor: http://741mhz.com/switch/

El compilador lo optimizará de todos modos; opte por el cambio, ya que es el más legible.

El Switch, aunque solo sea por legibilidad.En mi opinión, las declaraciones gigantes si son más difíciles de mantener y de leer.

ERROR_01 :// caída intencional

o

(ERROR_01 == númeroError) ||

El último es más propenso a errores y requiere más escritura y formato que el primero.

Código para facilitar la lectura.Si desea saber qué funciona mejor, utilice un generador de perfiles, ya que las optimizaciones y los compiladores varían, y los problemas de rendimiento rara vez están donde la gente cree que están.

Utilice switch, es para lo que sirve y lo que esperan los programadores.

Sin embargo, incluiría las etiquetas de casos redundantes; solo para que la gente se sintiera cómoda, estaba tratando de recordar cuándo y cuáles son las reglas para omitirlas.
No querrás que el próximo programador que trabaje en él tenga que pensar innecesariamente en los detalles del idioma (¡podrías ser tú dentro de unos meses!).

Los compiladores son realmente buenos optimizando switch.El gcc reciente también es bueno para optimizar un montón de condiciones en un if.

Hice algunos casos de prueba en rayo divino.

Cuando el case Los valores se agrupan muy juntos, gcc, clang e icc son lo suficientemente inteligentes como para usar un mapa de bits para verificar si un valor es uno de los especiales.

p.ej.gcc 5.2 -O3 compila el switch a (y el if algo muy parecido):

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

Tenga en cuenta que el mapa de bits son datos inmediatos, por lo que no hay posibles errores de acceso a la caché de datos ni a una tabla de salto.

gcc 4.9.2 -O3 compila el switch a un mapa de bits, pero ¿el 1U<<errNumber con mov/shift.Recopila el if versión a serie de ramas.

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

Observa cómo resta 1 de errNumber (con lea combinar esa operación con una mudanza).Eso le permite ajustar el mapa de bits en un formato inmediato de 32 bits, evitando el formato inmediato de 64 bits. movabsq que requiere más bytes de instrucción.

Una secuencia más corta (en código de máquina) sería:

    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

(La falta de uso jc fire_special_event es omnipresente y es un error del compilador.)

rep ret se utiliza en objetivos de rama y en las siguientes ramas condicionales, en beneficio de los antiguos AMD K8 y K10 (anteriores a Bulldozer): ¿Qué significa "rep ret"?.Sin él, la predicción de bifurcaciones no funciona tan bien en esas CPU obsoletas.

bt (prueba de bits) con un registro arg es rápido.Combina el trabajo de desplazar a la izquierda un 1 por errNumber bits y haciendo un test, pero sigue teniendo una latencia de 1 ciclo y solo un Intel uop.Es lento con un argumento de memoria debido a su semántica demasiado CISC:con un operando de memoria para la "cadena de bits", la dirección del byte que se va a probar se calcula en función del otro argumento (dividido por 8) y no se limita al fragmento de 1, 2, 4 u 8 bytes al que se apunta por el operando de memoria.

De Tablas de instrucciones de Agner Fog, una instrucción de cambio de conteo variable es más lenta que una bt en Intel reciente (2 uops en lugar de 1, y shift no hace todo lo demás que se necesita).

En mi opinión, este es un ejemplo perfecto de para qué se creó la falla del interruptor.

Si es probable que sus casos permanezcan agrupados en el futuro (si más de un caso corresponde a un resultado), el cambio puede resultar más fácil de leer y mantener.

Funcionan igual de bien.El rendimiento es aproximadamente el mismo en un compilador moderno.

Prefiero las declaraciones if a las case porque son más legibles y más flexibles; puede agregar otras condiciones que no se basen en la igualdad numérica, como " || max < min ".Pero para el caso simple que publicaste aquí, realmente no importa, solo haz lo que te resulte más legible.

Definitivamente se prefiere el interruptor.Es más fácil ver la lista de casos de un interruptor y saber con certeza qué está haciendo que leer la condición if larga.

La duplicación en el if La condición es dura para los ojos.Supongamos que uno de los == fue escrito !=;¿te darías cuenta?¿O si una instancia de 'numError' se escribiera como 'nmuError', que simplemente se compiló?

Generalmente prefiero usar polimorfismo en lugar del interruptor, pero sin más detalles del contexto, es difícil decirlo.

En cuanto al rendimiento, lo mejor que puede hacer es utilizar un generador de perfiles para medir el rendimiento de su aplicación en condiciones similares a las que espera en la naturaleza.De lo contrario, probablemente esté optimizando en el lugar equivocado y de forma incorrecta.

Estoy de acuerdo con la compacidad de la solución de cambio, pero en mi opinión, estás secuestrando el interruptor aquí.
El propósito del interruptor es tener diferente Manejo dependiendo del valor.
Si tuvieras que explicar tu algo en pseudocódigo, usarías un if porque, semánticamente, eso es lo que es: si cualquier_error hace esto...
Entonces, a menos que algún día tengas la intención de cambiar tu código para tener un código específico para cada error, usaría si.

No estoy seguro de las mejores prácticas, pero usaría el interruptor y luego atraparía la caída intencional a través de "predeterminado".

Estéticamente tiendo a favorecer este enfoque.

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

Hacer que los datos sean un poco más inteligentes para que podamos hacer la lógica un poco más tonta.

Me doy cuenta de que parece raro.Aquí está la inspiración (de cómo lo haría en 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)

Probablemente el primero esté optimizado por el compilador, eso explicaría por qué el segundo bucle es más lento al aumentar el número de bucles.

Yo elegiría la declaración if en aras de la claridad y la convención, aunque estoy seguro de que algunos no estarían de acuerdo.Después de todo, estás queriendo hacer algo. if ¡alguna condición es cierta!Tener un interruptor con una sola acción parece un poco...innecesario.

No soy la persona que le puede hablar sobre la velocidad y el uso de la memoria, pero mirar una declaración de cambio es mucho más fácil de entender que una declaración if grande (especialmente 2-3 meses después).

Yo diría que use SWITCH.De esta manera sólo tendrá que implementar diferentes resultados.Sus diez casos idénticos pueden utilizar el valor predeterminado.Si se realiza un cambio, todo lo que necesita es implementar explícitamente el cambio, no es necesario editar el valor predeterminado.También es mucho más fácil agregar o eliminar casos de un SWITCH que editar IF y ELSEIF.

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

Tal vez incluso pruebe su condición (en este caso el número) con una lista de posibilidades, tal vez una matriz para que su INTERRUPTOR ni siquiera se use a menos que definitivamente haya un resultado.

Dado que solo tiene 30 códigos de error, codifique su propia tabla de salto y luego haga todas las opciones de optimización usted mismo (el salto siempre será el más rápido), en lugar de esperar que el compilador haga lo correcto.También hace que el código sea muy pequeño (aparte de la declaración estática de la tabla de salto).También tiene el beneficio adicional de que con un depurador puedes modificar el comportamiento en tiempo de ejecución si así lo necesitas, simplemente ingresando los datos de la tabla directamente.

Sé que es viejo pero

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

Variar el recuento de bucles cambia mucho:

Mientras que si/si no:Switch 5ms:1Ms Max Loops:100000

Mientras que si/si no:Switch 5ms:3 ms máximos bucles:1000000

Mientras que si/si no:Switch 5ms:14 ms máximos bucles:10000000

Mientras que si/si no:Switch 5ms:149ms Max Loops:100000000

(agregue más declaraciones si lo desea)

A la hora de compilar el programa, no sé si hay alguna diferencia.Pero en cuanto al programa en sí y a mantener el código lo más simple posible, personalmente creo que depende de lo que quieras hacer.Las declaraciones if else if else tienen sus ventajas, que creo que son:

Permitirle probar una variable en rangos específicos, puede usar funciones (biblioteca estándar o personal) como condicionales.

(ejemplo:

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

Sin embargo, las declaraciones If else if else pueden volverse complicadas y confusas (a pesar de sus mejores intentos) rápidamente.Las declaraciones de cambio tienden a ser más claras, limpias y fáciles de leer;pero solo se puede utilizar para realizar pruebas con valores específicos (ejemplo:

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

Prefiero declaraciones if - else if - else, pero realmente depende de usted.Si desea utilizar funciones como condiciones, o desea probar algo con un rango, matriz o vector y/o no le importa lidiar con el anidamiento complicado, le recomendaría usar bloques If else if else.Si desea realizar pruebas con valores únicos o desea un bloque limpio y fácil de leer, le recomendaría que utilice bloques de mayúsculas y minúsculas switch().

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top