c++ - resueltos - Ventaja de cambiar la instrucción if-else
sentencia if else en c++ (23)
Código de legibilidad 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.
¿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.
declaración 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
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();
}
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 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 así lo necesita, simplemente introduciendo los datos de la tabla directamente.
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 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 intentos) 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: "<<a<<"/n";
break;
//other case statements
default:
cout<<"a is not between the range or is not a good value/n"
break;
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 ().
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 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 compilará
La mayoría de las personas usará define (¡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 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 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 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 de Usar conmutador aumentará 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 compilador 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.
Escogería la declaración 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.
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()
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 .
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 estén basadas 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.
IMO este es un ejemplo perfecto de para qué se creó el cambio de interruptor.
Los compiladores 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 mapa de bits para comprobar si un valor es uno de los especiales.
por ejemplo, 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()
Tenga en cuenta que el mapa de bits 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 compila el switch
a un mapa de bits, pero hace el 1U<<errNumber
con mov / shift. Compila la versión if
para 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()
Observe cómo resta 1 de errNumber
(con lea
para combinar esa operación con un movimiento). Eso le permite encajar el mapa de bits 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 compilador ).
rep ret
se utiliza en los destinos de sucursal y en las siguientes ramas 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).
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''
No soy la persona que le dice acerca de 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 grande (especialmente 2-3 meses en la línea)
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.
Por favor use el interruptor. La instrucción if llevará tiempo proporcional a la cantidad de condiciones.
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)
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.
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).
Use el interruptor, es para lo que es y lo que los programadores esperan.
Sin embargo, me gustaría poner las etiquetas 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 programador que trabaje en él tenga que pensar innecesariamente sobre los detalles del lenguaje (¡podrías ser tú dentro de unos meses!)
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.
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 compilar?
Por lo general, prefiero usar el polimorfismo 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.
while (true) != while (loop)
Probablemente, el primero sea optimizado por el compilador, eso explicaría por qué el segundo ciclo es más lento al aumentar el conteo de bucles.