140

¿Cuál es la mejor práctica para usar una declaración switch frente al uso de una declaración if para 30 unsigned enumeraciones donde aproximadamente 10 tienen una acción esperada (que actualmente es la misma acción). El rendimiento y el espacio deben considerarse pero no son críticos. He abstraído el fragmento así que no me odien por las convenciones de nomenclatura.Ventaja del cambio sobre la instrucción if-else

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(); 
} 
+21

¿Esto está editado como 'subjetivo'? De Verdad? ¿Seguramente "subjetivo" es para cosas que no pueden ser probadas de una manera u otra? –

+0

Claro que puedes verlo desde el punto de que genera el código más eficiente, pero cualquier compilador moderno debería ser igualmente eficiente. Al final, esto es más una cuestión del color del cobertizo de la bicicleta. – jfs

+7

No estoy de acuerdo, no creo que esto sea subjetivo. Una diferencia simple de ASM importa, en muchos casos no se puede ignorar unos pocos segundos de optimización.Y en esta pregunta, no se trata de una guerra o debate religioso, hay una explicación racional de por qué uno sería más rápido, solo lea la respuesta aceptada. – chakrit

Respuesta

140

Use el interruptor.

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

En el mejor de los casos, el optimizador puede encontrar una forma mejor de generar el código. Las cosas comunes que hace un compilador es construir un árbol de decisión binario (guarda comparaciones y saltos en el caso promedio) o simplemente crea una tabla de salto (funciona sin comparación en absoluto).

+1

Técnicamente todavía habrá una comparación, para asegurarse de que el valor de la enumeración esté dentro de la tabla de salto. – 0124816

+0

Jep. Es verdad. Sin embargo, encender enums y manejar todos los casos puede deshacerse de la última comparación. –

+0

¿Alguien puede editar esto para cambiarlo y perderlo? –

18

compilador optimizar todos modos - ir para el interruptor, ya que es la más legible.

+3

Es probable que el compilador no toque if-then-else. De hecho, 'gcc' no lo hará seguro (hay una buena razón para eso). Clang optimizará ambos casos en una búsqueda binaria. Por ejemplo, vea [esto] (http://lazarenko.me/2013/01/13/switch-statement-machine-code/). –

6

El interruptor, aunque solo sea por la legibilidad. Las declaraciones gigantes si son más difíciles de mantener y más difíciles de leer en mi opinión.

ERROR_01: // intencional caída-a través de

o

(ERROR_01 == numError) ||

Lo posterior es más propenso a errores y requiere más tipeo y formateo que el primero.

1

no estoy seguro acerca de las mejores prácticas, pero que haría uso de interruptor - y luego trampa intencional caída de acceso vía 'por defecto'

+0

espera, ... ¿qué? –

2

OMI Este es un ejemplo perfecto de lo que cambiar de paso al siguiente se hizo para.

+0

en C# este es el único caso donde ocurre el pensamiento fallido. Buen argumento allí mismo. – BCS

0

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

1

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

0

Utilice el interruptor. La instrucción if llevará tiempo proporcional a la cantidad de condiciones.

0

no soy la persona para informarle sobre la velocidad y el uso de la memoria, pero mirando a un DECLARACIÓN interruptor es un infierno de mucho más fácil de entender y luego un gran if (especialmente los 2-3 meses en la línea)

1

Funcionan igual de bien. El rendimiento es casi el mismo dado un compilador moderno.

Prefiero las declaraciones sobre las declaraciones de casos 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í, en realidad no importa, solo haz lo que te sea más fácil de leer.

1

interruptor es definitivamente preferido. Es más fácil ver la lista de casos de un interruptor & saber con certeza qué está haciendo que leer la condición larga si.

La duplicación en la condición if es dura para los ojos. Supongamos que uno de los == fue escrito !=; lo notarías? ¿O si una instancia de 'numError' fue escrita 'nmuError', que acaba de compilar?

En general, preferiría usar polimorfismo en lugar del interruptor, pero sin más detalles del contexto, es difícil de decir.

En cuanto a rendimiento, su mejor opción 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és optimizando en el lugar equivocado y de la manera incorrecta.

0

Yo diría usar SWITCH. De esta manera, solo tienes que implementar diferentes resultados. Sus diez casos idénticos pueden usar el predeterminado. Si uno cambia todo lo que necesita es implementar explícitamente el cambio, no es necesario editar el predeterminado. También es mucho más fácil agregar o quitar mayúsculas de un SWITCH que editar IF y ELSEIF.

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

Tal vez incluso probar su condición (en este caso numerror) en contra de una lista de posibilidades, una matriz tal vez para su conmutador ni siquiera se utiliza a menos que definitivamente habrá un resultado.

+0

Hay alrededor de 30 errores en total. 10 requieren la acción especial, así que estoy usando el valor predeterminado para los ~ 20 errores que no requieren una acción ... –

+0

¡Completamente se equivocó de palo! – lewis

1

Estoy de acuerdo con la compacidad de la solución del interruptor, pero IMO eres secuestrando el interruptor aquí.
El propósito del interruptor es tener diferente manejo de dependiendo del valor.
Si tuviera que explicar algo en su pseudo-código, tendrá que utilizar un if ya que, semánticamente, eso es lo que es: si whatever_error hacer esto ...
Así que a menos que vaya algún día a cambiar el código para tiene un código específico para cada error, yo usaría si.

+2

No estoy de acuerdo, por la misma razón que estoy en desacuerdo con el caso fallido. Leí el interruptor como "En los casos 01,07,0A, 10,15,16 y 20 evento especial de fuego". No hay caída en otra sección. Esto es solo un artefacto de la sintaxis de C++ donde se repite la palabra clave 'caso' para cada valor. – MSalters

0

Como solo tiene 30 códigos de error, codifique su propia tabla de saltos, luego realice todas las elecciones de optimización usted mismo (el salto siempre será 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 puede modificar el comportamiento en tiempo de ejecución en caso de que lo necesite, simplemente introduciendo los datos de la tabla directamente.

+0

Guau, parece una manera de convertir un problema simple en uno complejo. ¿Por qué ir a todos esos problemas cuando el compilador hará un gran trabajo para usted? Además, aparentemente es un controlador de errores, por lo que no es tan crítico para la velocidad. Un cambio es, con mucho, lo más fácil de leer y mantener. – MrZebra

+0

Una tabla no es compleja, de hecho, es probablemente más simple que un cambio de código. Y la declaración mencionó el rendimiento fue un factor. –

+0

Eso suena como una optimización prematura. Mientras mantenga sus valores enum pequeños y contiguos, el compilador debería hacerlo por usted. Al poner el interruptor en una función separada, el código que lo usa es pequeño y agradable, como sugiere Mark Ransom en su respuesta, ofrece el mismo beneficio de código pequeño. –

18

El interruptor es más rápido.

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

Ahora, el interruptor tiene un problema real: El conmutador debe saber en tiempo de 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á.

mayoría de las personas a continuación, utilizar define (Aargh!), Y otros declarar y definir las variables constantes en la misma unidad de compilación. Por ejemplo:

Así que, al final, el revelador debe elegir entre "la velocidad de la claridad +" frente a "código de acoplamiento".

(No es que un interruptor no se pueda escribir para ser confuso como el infierno ... La mayoría del cambio que veo actualmente son de esta categoría "confusa" ... Pero esta es otra historia ...)

Editar 2008-09-21:

bk1e añadió el siguiente comentario: "Definiendo constantes como las enumeraciones en un archivo de cabecera es otra manera de manejar esto"

por supuesto que es.

El punto de un tipo extern era para 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 subrayar el valor. Por lo tanto, si la definición, el valor de enum o el valor de configuración cambian, se necesitaría 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 que sea imposible usar el interruptor. 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, entonces usa el interruptor. Cuando no lo es, entonces, no es sorpresa.

.

Edición 15/01/2013:

Vlad Lazarenko comentado en mi respuesta, dando un enlace a su estudio en profundidad del código ensamblador generado por un interruptor. Muy Enlightning: http://741mhz.com/switch/

+0

La definición de constantes como enumeraciones en un archivo de encabezado es otra forma de manejar esto. – bk1e

+5

El interruptor es [no __ siempre__ más rápido] (http://lazarenko.me/2013/01/13/switch-statement-machine-code/). –

+1

@Vlad Lazarenko: ¡Gracias por el enlace! Fue una lectura muy interesante. – paercebal

4

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

41

Para el caso especial de que usted ha proporcionado en su ejemplo, el código más claro es probablemente:

if (RequiresSpecialEvent(numError)) 
    fire_special_event(); 

Obviamente esto sólo traslada el problema a un área diferente del código, pero ahora tiene la oportunidad para reutilizar esta prueba. También tienes más opciones sobre cómo resolverlo. Se podría utilizar std :: set, por ejemplo:

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

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

+4

Esto es tan cierto. La legibilidad es mucho mejor que las declaraciones if y switch. En realidad iba a responder algo así, pero me ganaste. :-) – mlarsen

+0

Si sus valores enum son todos pequeños, entonces no necesita un hash, solo una tabla. p.ej. 'const std :: bitset specialerror (inicializador);' Úselo con 'if (specialerror [numError]) {fire_special_event(); } ' Si desea comprobar los límites, 'bitset :: test (size_t)' lanzará una excepción en los valores fuera de límites. ('bitset :: operator []' no controla el rango). http://www.cplusplus.com/reference/bitset/bitset/test/. Esto probablemente superará a una tabla de salto generada por el compilador que implementa 'switch', esp. en el caso no especial donde esta será una rama única no tomada. –

+0

@PeterCordes Sigo sosteniendo que es mejor poner la mesa en su propia función. Como dije, hay * muchas * opciones que se abren cuando haces eso, no intenté enumerarlas todas. –

1

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

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

Me doy cuenta de que se ve raro. Aquí está la inspiración (de la forma en que 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() 
+4

La sintaxis de un idioma * no * tiene un efecto sobre cómo implementamos una solución ... => Parece feo en C y agradable en Python. :) – rlerallut

+0

¿Usar mapas de bits? Si error_0a es 0x0a, etc. podrías ponerlos como bits en un largo tiempo. long long special_events = 1LL << 1 | 1LL << 7 | 1LL << 0xa ... Luego use if (special_events & (1LL << numError) fire_special_event() – paperhorse

+1

Yuck. Ha convertido una O (1) operación de peor caso (si se generan tablas de salto) en O (N) el peor de los casos (donde N es la cantidad de casos manejados), ** y ** usaste un 'break' fuera de un' case' (sí, un pecado menor, pero un pecado, no obstante). :) – Mac

5

Uso del interruptor, que es para lo que sirve y lo que los programadores esperar.

Sin embargo, pondría las etiquetas de casos redundantes; solo para hacer que las personas se sientan cómodas, estaba tratando de recordar cuándo/cuáles son las reglas para dejarlas fuera.
usted no quiere que la próxima programador que trabaja en ella a tener que hacer ningún pensamiento innecesaria sobre los detalles del lenguaje (que usted podría ser en unos pocos meses!)

0

Sé que es antiguo, 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); 
} 
} 

la variación de la cuenta de bucles cambia mucho:

Mientras if/else: 5 ms interruptor: 1 ms máx Loops: 100000

Mientras if/else: 5 ms Switch: 3 ms Max Loops: 1000000

Mientras if/else: 5 ms interruptor: 14ms Max Loops: 10000000

Mientras if/else: 5 ms interruptor: 149ms Max Loops: 100000000

(añadir más declaraciones si quieres)

+3

Buen punto, pero sry, amigo, estás en el idioma equivocado. Variando el idioma cambia mucho;) –

+2

El bucle 'if (max) break' se ejecuta en tiempo constante independientemente del recuento de bucles? Parece que el compilador JIT es lo suficientemente inteligente como para optimizar el bucle a 'counter2 = max'. Y tal vez sea más lento que cambiar si la primera llamada a 'currentTimeMillis' tiene más sobrecarga, porque todavía no todo está compilado en JIT. Poner los bucles en el otro orden probablemente daría resultados diferentes. –

1
while (true) != while (loop) 

Probablemente, la primera de ellas está optimizado por el compilador, que explicaría por qué t El segundo ciclo es más lento al aumentar el conteo de bucles.

+0

Esto parece ser un comentario a la respuesta de McAnix. Ese es solo uno de los problemas con ese intento de sincronizar 'if' versus' switch' como una condición de final de bucle en Java. –

2

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

Hice algunos casos de prueba en godbolt.

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

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

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

en cuenta que el mapa de bits son datos inmediatos, así que no hay potencial miss-caché de datos para acceder a él, o una tabla de saltos.

gcc 4.9.2 -O3 compila el switch a un mapa de bits, pero lo hace el 1U<<errNumber con mov/shift. Compila la versión if a series 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() 

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

A (en código de máquina) secuencia más corta 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 de jc fire_special_event es omnipresente, y es a compiler bug)

rep ret se utiliza en objetivos de las ramificaciones, y siguiendo las ramas condicionales, en beneficio de los antiguos AMD K8 y K10 (pre-Bulldozer): What does `rep ret` mean?. Sin él, la predicción de bifurcación no funciona tan bien en esas CPU obsoletas.

bt (prueba de bit) con un registro arg es rápido. Combina el trabajo del desplazamiento a la izquierda de un 1 por errNumber bits y haciendo un test, pero sigue siendo 1 ciclo de latencia y solo un único Intel UOP. Es lento con una memoria arg debido a su semántica de modo 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 de la otra arg (dividida por 8), e isn No se limita al fragmento de 1, 2, 4 u 8 bytes al que apunta el operando de memoria.

De Agner Fog's instruction tables, una instrucción de cambio de cuenta variable es más lenta que bt en Intel reciente (2 uops en lugar de 1, y shift no hace todo lo demás).

0

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

le permiten probar una variable contra 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, si otra persona si else puede ser complicado y desordenado (a pesar de sus mejores intentos) en una prisa Cambie declaraciones tienden a ser más claro, más limpio y más fácil de leer; pero sólo se puede utilizar para poner a prueba contra los 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 si - if else - else, pero es realmente depende de ti Si desea utilizar funciones como las condiciones, o usted. desea probar algo en un rango, matriz o vector y/o no lo hace Al lidiar con el complicado anidamiento, recomendaría usar If else if else blocks. Si desea probar contra valores únicos o si desea un bloque limpio y fácil de leer, le recomendaría que use bloques de mayúsculas y minúsculas().

Cuestiones relacionadas