Ventaja de cambiar la instrucción if-else

¿Cuál es la mejor práctica para usar una sentencia switch frente a usar una instrucción if para 30 enumeraciones unsigned 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.

statement de switch :

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

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

Use el interruptor

En el peor de los casos, el comstackdor 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 statement 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 comstackdor 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).

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

 if (RequiresSpecialEvent(numError)) fire_special_event(); 

Obviamente, esto solo mueve el problema a un área diferente del código, pero ahora tiene la oportunidad de reutilizar esta prueba. También tienes más opciones sobre cómo resolverlo. Puede 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 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.

El cambio es más rápido.

Intente con if / else-ing 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 comstackció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 comstackrá

La mayoría de las personas usará define (¡Aargh!), Y otros declararán y definirán variables constantes en la misma unidad de comstackció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 un interruptor no se pueda escribir para ser confuso como el infierno … La mayoría del cambio que veo actualmente es de esta categoría “confusa” … Pero esta es otra historia …)

Editar 2008-09-21:

bk1e agregó el siguiente comentario: ” Definir constantes como enumeraciones en un archivo de encabezado es otra forma de manejar esto”.

Por supuesto que es.

El objective de un tipo externo era desacoplar el valor de la fuente. Definir este valor como una macro, como una simple statement 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 recomstackción. La statement externa significa que no hay necesidad de volver a comstackr en caso de cambio de valor, pero por otro lado, hace que sea imposible usar el interruptor. La conclusión de Usar conmutador boostá el acoplamiento entre el código del conmutador y las variables utilizadas como casos . Cuando está bien, entonces usa el interruptor. Cuando no lo es, entonces, no es sorpresa.

.

Editar 2013-01-15:

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

El comstackdor lo optimizará de todos modos: elija el conmutador ya que es el más legible.

El interruptor, aunque solo sea por 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 : // caída intencional

o

(ERROR_01 == numError) ||

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

Use el interruptor, es para lo que es y lo que los progtwigdores esperan.

Sin embargo, me gustaría poner las tags de casos redundantes; solo para hacer que las personas se sintieran cómodas, estaba tratando de recordar cuándo / cuáles son las reglas para dejarlas fuera.
No querrás que el próximo progtwigdor que trabaje en él tenga que pensar innecesariamente sobre los detalles del lenguaje (¡podrías ser tú dentro de unos meses!)

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

IMO este es un ejemplo perfecto de para qué se creó el cambio de interruptor.

Los comstackdores son realmente buenos para optimizar el 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 de case se agrupan muy juntos, gcc, clang e icc son lo suficientemente inteligentes como para usar un bitmap para comprobar si un valor es uno de los especiales.

por ejemplo, gcc 5.2 -O3 comstack 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() 

Tenga en cuenta que el bitmap es datos inmediatos, por lo que no hay datos potenciales de caché de falta de acceso a él, o una mesa de salto.

gcc 4.9.2 -O3 comstack el switch a un bitmap, pero hace el 1U< con mov / shift. Comstack la versión if para series de twigs.

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

Observe cómo resta 1 de errNumber (con lea para combinar esa operación con un movimiento). Eso le permite encajar el bitmap en un inmediato de 32 bits, evitando el movabsq inmediato de 64 bits 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 falla en el uso de jc fire_special_event es omnipresente y es un error del comstackdor ).

rep ret se utiliza en los destinos de sucursal y en las siguientes twigs condicionales, en beneficio de los antiguos AMD K8 y K10 (pre-Bulldozer): ¿Qué significa `rep ret`? . 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 a 1 con bits errNumber y realizando una test , pero sigue siendo 1 latencia de ciclo 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 las 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).

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.

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

Prefiero las declaraciones sobre las declaraciones de casos porque son más legibles y más flexibles: puede agregar otras condiciones que no estén basadas en la igualdad numérica, como “|| max

el interruptor es definitivamente preferido. 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 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 comstackr?

Por lo general, prefiero usar el polymorphism en lugar del interruptor, pero sin más detalles del contexto, es difícil de decir.

En cuanto al rendimiento, la 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.

Estoy de acuerdo con la compacidad de la solución de conmutación, pero la OMI está secuestrando el interruptor aquí.
El propósito del cambio es tener un manejo diferente dependiendo del valor.
Si tuviera que explicar su algo en seudocódigo, usaría un if porque, semánticamente, eso es lo que es: if whatever_error haga esto
Así que a menos que intente algún día cambiar su código para tener un código específico para cada error, usaría if .

No estoy seguro acerca 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; } } } 

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

Me doy cuenta de que se ve 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 sea optimizado por el comstackdor, eso explicaría por qué el segundo ciclo es más lento al boost el conteo de bucles.

Escogería la statement if por claridad y convención, aunque estoy seguro de que algunos estarían en desacuerdo. ¡Después de todo, quieres hacer algo if alguna condición es verdadera! Tener un interruptor con una acción parece un poco … innecesario.

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

No soy la persona que le dice acerca de la velocidad y el uso de la memoria, pero mirar una statement de cambio es mucho más fácil de entender que una statement grande (especialmente 2-3 meses en la línea)

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 pruebe su condición (en este caso, numerror) contra una lista de posibilidades, una matriz tal vez para que su INTERRUPTOR ni siquiera se use a menos que definitivamente haya un resultado.

Como solo tienes 30 códigos de error, codifica tu propia tabla de saltos, luego tomas todas las decisiones de optimización tú mismo (el salto siempre será más rápido), en lugar de esperar que el comstackdor haga lo correcto. También hace que el código sea muy pequeño (aparte de la statement 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 si así lo necesita, simplemente introduciendo 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); } } 

Variando el recuento de bucles cambia mucho:

Mientras que if / else: Switch de 5ms: 1ms Max Loops: 100000

Mientras que if / else: interruptor de 5ms: bucles máximos de 3ms: 1000000

Mientras que if / else: Switch de 5ms: 14ms Max Loops: 10000000

Mientras que if / else: Switch de 5ms: 149ms Max Loops: 100000000

(agregue más declaraciones si lo desea)

Cuando se trata de comstackr el progtwig, no sé si hay alguna diferencia. Pero en cuanto al progtwig 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 las declaraciones tienen sus ventajas, que creo que son:

le permite 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 no es así, las declaraciones pueden complicarse y complicarse (a pesar de sus mejores bashs) con prisa. Las declaraciones de cambio tienden a ser más claras, más limpias y más fáciles de leer; pero solo se puede usar para probar contra 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: "< 

Prefiero if - else if - else declaraciones, pero realmente depende de ti. Si desea usar funciones como las condiciones, o si desea probar algo en un rango, matriz o vector y / o no le importa manejar el complicado anidamiento, le 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 ().