¿Cuál es la mejor práctica para usar una switch
declaración en lugar de usar una if
declaración para 30 unsigned
enumeraciones donde aproximadamente 10 tienen una acción esperada (que actualmente es la misma acción)? Es necesario tener en cuenta el rendimiento y el espacio, pero no son críticos. He abstraído el fragmento, así que no me odies por las convenciones de nombres.
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();
}
Respuestas:
Use el interruptor.
En el peor de los casos, el compilador generará el mismo código que una cadena if-else, para que no pierda nada. En caso de duda, coloque los casos más comunes primero en la declaración de cambio.
En el mejor de los casos, el optimizador puede encontrar una mejor manera 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 construir una tabla de salto (funciona sin comparaciones).
fuente
if
-else
cadenas en la programación plantilla meta, y la dificultad de generarswitch
case
cadenas, que la cartografía puede llegar a ser más importante. (y sí, comentario antiguo, pero la web es para siempre, o al menos hasta el próximo martes)Para el caso especial que ha proporcionado en su ejemplo, el código más claro es probablemente:
Obviamente, esto solo mueve el problema a un área diferente del código, pero ahora tiene la oportunidad de reutilizar esta prueba. También tiene más opciones sobre cómo resolverlo. Puede usar std :: set, por ejemplo:
No estoy sugiriendo que esta sea la mejor implementación de RequiereSpecialEvent, solo que es una opción. Todavía puede 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 sea su proceso de decisión, más valor obtendrá de tenerlo en una función aislada.
fuente
const std::bitset<MAXERR> specialerror(initializer);
Úselo conif (specialerror[numError]) { fire_special_event(); }
. Si desea verificar los límites,bitset::test(size_t)
lanzará una excepción en los valores fuera de los límites. (bitset::operator[]
no comprueba el rango). cplusplus.com/reference/bitset/bitset/test . Esto probablemente superará a la implementación de una tabla de salto generada por el compiladorswitch
, especialmente. en el caso no especial donde será una sola rama no tomada.std::set
, pensé en señalar que probablemente sea una mala elección. Resulta que gcc ya compila el código del OP para probar un mapa de bits en un bit de 32 bits inmediato. godbolt: goo.gl/qjjv0e . gcc 5.2 incluso hará esto para laif
versión. Además, gcc más reciente utilizará la instrucción de prueba de bits enbt
lugar de cambiar para colocar un1
bit en el lugar correcto y usartest reg, imm32
.El cambio es más rápido.
Simplemente intente si 30 valores diferentes dentro de un bucle y compárelo con el mismo código usando el interruptor para ver qué tan rápido es el interruptor.
Ahora, el interruptor tiene un problema real : el interruptor debe conocer en tiempo de compilación los valores dentro de cada caso. Esto significa que el siguiente código:
no se compilará
La mayoría de la gente usará define (¡Aargh!), Y otros declararán y definirán variables constantes en la misma unidad de compilación. Por ejemplo:
Entonces, al final, el desarrollador debe elegir entre "velocidad + claridad" versus "acoplamiento de código".
(No es que un interruptor no se pueda escribir para ser confuso como el infierno ... La mayoría del interruptor que veo actualmente son de esta categoría "confusa" ... Pero esta es otra historia ...)
.
fuente
El compilador lo optimizará de todos modos: elija el interruptor, ya que es el más legible.
fuente
gcc
no lo haré con seguridad (hay una buena razón para eso). Clang optimizará ambos casos en una búsqueda binaria. Por ejemplo, mira esto .El Switch, aunque solo sea por legibilidad. Gigante si las declaraciones 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 formato que el primero.
fuente
Código de legibilidad. Si desea saber qué funciona mejor, use un generador de perfiles, ya que las optimizaciones y los compiladores varían, y los problemas de rendimiento rara vez se encuentran donde las personas piensan que están.
fuente
Use el interruptor, es para qué sirve y qué esperan los programadores.
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.
No desea que el próximo programador que trabaje en él tenga que pensar innecesariamente sobre los detalles del idioma (¡podría ser usted dentro de unos meses!)
fuente
Los compiladores son muy buenos para optimizar
switch
. El gcc reciente también es bueno para optimizar un montón de condiciones en unif
.Hice algunos casos de prueba en Godbolt .
Cuando el
case
valores se agrupan 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.Por ejemplo, gcc 5.2 -O3 compila el
switch
to (yif
algo muy similar):Tenga en cuenta que el mapa de bits son datos inmediatos, por lo que no hay una posible falta de caché de datos para acceder a él, o una tabla de salto.
gcc 4.9.2 -O3 compila el
switch
en un mapa de bits, pero lo hace1U<<errNumber
con mov / shift. Recopila laif
versión a series de ramas.Observe cómo resta 1 de
errNumber
(conlea
para combinar esa operación con un movimiento). Eso le permite ajustar el mapa de bits en un inmediato de 32 bits, evitando el inmediato de 64 bitsmovabsq
que requiere más bytes de instrucciones.Una secuencia más corta (en código máquina) sería:
(La omisión de uso
jc fire_special_event
es omnipresente y es un error del compilador ).rep ret
se usa en objetivos de ramificación, y siguiendo ramificaciones condicionales, en beneficio de los viejos AMD K8 y K10 (pre-Bulldozer): ¿Qué significa `rep ret`? . Sin ella, la predicción de rama no funciona tan bien en esas CPU obsoletas.bt
(prueba de bit) con un registro arg es rápido. Combina el trabajo de desplazar a la izquierda un 1 porerrNumber
bits y hacer untest
, pero sigue siendo una latencia de 1 ciclo y solo una única Intel uop. Es lento con un argumento de memoria debido a su semántica CISC: con un operando de memoria para la "cadena de bits", la dirección del byte a probar se calcula en función del otro argumento (dividido por 8), e isn no se limita al fragmento de 1, 2, 4 u 8 bytes señalado por 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
de Intel reciente (2 uops en lugar de 1, y shift no hace todo lo demás que se necesita).fuente
Las ventajas de switch () sobre si el caso else son: 1. El cambio es mucho más eficiente en comparación con if else porque cada caso no depende del caso anterior, a diferencia de lo contrario, donde la declaración individual debe verificarse para determinar si es verdadera o falsa.
Cuando hay un no. de los valores para una sola expresión, el cambio de mayúsculas y minúsculas es más flexible en caso de que si no, porque en caso contrario, el juicio se basa en solo dos valores, es decir, verdadero o falso.
Los valores en el conmutador están definidos por el usuario, mientras que los valores en caso de que sí se basan en restricciones.
En caso de error, las declaraciones en el interruptor pueden verificarse y corregirse fácilmente, lo que es relativamente difícil de verificar en caso de que se haga otra declaración.
La caja del interruptor es mucho más compacta y fácil de leer y comprender.
fuente
En mi opinión, este es un ejemplo perfecto de para qué se hizo la interrupción del interruptor.
fuente
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.
fuente
Funcionan igualmente bien. El rendimiento es casi el mismo dado un compilador moderno.
Prefiero las declaraciones if sobre las declaraciones 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 sea más legible para ti.
fuente
El interruptor es definitivamente preferido. Es más fácil mirar la lista de casos de un interruptor y saber con certeza lo que está haciendo que leer la condición larga.
La duplicación en la
if
condición es dura para los ojos. Supongamos que uno de los==
fue escrito!=
; te darias cuenta? ¿O si una instancia de 'numError' se escribió 'nmuError', que acaba de compilarse?Generalmente prefiero usar el polimorfismo en lugar del interruptor, pero sin más detalles del contexto, es difícil de decir.
En cuanto al rendimiento, lo mejor 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 incorrecto y de la manera incorrecta.
fuente
Estoy de acuerdo con la compatibilidad de la solución del interruptor, pero en mi opinión, estás secuestrando el interruptor aquí.
El propósito del interruptor es tener un manejo diferente según el valor.
Si tuviera que explicar su algo en pseudocódigo, usaría un if porque, semánticamente, eso es lo que es: if whatever_error hace esto ...
Entonces, a menos que tenga la intención de cambiar algún día su código para tener un código específico para cada error , Lo usaría si .
fuente
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, ¡estás deseando hacer algo
if
que sea cierto! Tener un interruptor con una sola acción parece un poco ... innecesario.fuente
Yo diría que use SWITCH. De esta manera solo tiene 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 valor predeterminado. También es mucho más fácil agregar o quitar casos de un SWITCH que editar IF y ELSEIF.
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.
fuente
Como solo tiene 30 códigos de error, codifique su propia tabla de salto, luego haga 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 si lo necesita, simplemente introduciendo los datos de la tabla directamente.
fuente
std::bitset<MAXERR> specialerror;
, entoncesif (specialerror[err]) { special_handler(); }
. Esto será más rápido que una mesa de salto, especialmente. en el caso no tomadoNo estoy seguro de las mejores prácticas, pero usaría el interruptor, y luego atraparía la falla intencional a través de 'predeterminado'
fuente
Estéticamente tiendo a favorecer este enfoque.
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 cómo lo haría en Python):
fuente
break
exterior acase
(sí, un menor pecado, pero un pecado no obstante). :)Probablemente el primero está optimizado por el compilador, eso explicaría por qué el segundo ciclo es más lento al aumentar el conteo de ciclos.
fuente
if
vs.switch
como una condición de final de ciclo en Java.Cuando se trata 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. if else if else, las declaraciones tienen sus ventajas, que creo son:
le permite probar una variable contra rangos específicos; puede usar funciones (Biblioteca estándar o Personal) como condicionales.
(ejemplo:
Sin embargo, las declaraciones de If else if else pueden volverse complicadas y desordenadas (a pesar de sus mejores intentos) a toda 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 valores específicos (ejemplo:
Prefiero las declaraciones if - else if - else, pero realmente depende de usted. Si desea utilizar funciones como las condiciones, o desea probar algo contra un rango, matriz o vector y / o no le importa tratar con la complicada anidación, recomendaría usar bloques If if if else. Si desea probar con valores individuales o si desea un bloque limpio y fácil de leer, le recomendaría que use bloques de mayúsculas y minúsculas ().
fuente
No soy la persona que le dirá sobre la velocidad y el uso de la memoria, pero mirar una declaración de interruptor es mucho más fácil de entender que una declaración if grande (especialmente 2-3 meses más adelante)
fuente
Sé que es viejo pero
Variar el recuento de bucles cambia mucho:
Mientras que si / si no: 5ms Switch: 1ms Max Loops: 100000
Mientras que si / si no: 5ms Switch: 3ms Max Loops: 1000000
Mientras que si / si no: 5ms Switch: 14ms Max Loops: 10000000
Mientras que si / si no: 5ms Switch: 149ms Max Loops: 100000000
(agregue más declaraciones si lo desea)
fuente
if(max) break
ciclo se ejecuta en tiempo constante independientemente del recuento de ciclos? Parece que el compilador JIT es lo suficientemente inteligente como para optimizar el bucle de distanciacounter2=max
. Y tal vez sea más lento que cambiar si la primera llamadacurrentTimeMillis
tiene más sobrecarga, porque ¿aún no está todo compilado JIT? Poner los bucles en el otro orden probablemente daría resultados diferentes.