utiliza switch sintaxis sentencia que programacion para lenguaje else ejemplos comando c++ performance c++11 assembly microbenchmark

switch - sentencia if else en c++



Sentencia if versus sentencia if-else, ¿cuál es más rápido? (6)

Discutí con un amigo el otro día sobre esos dos fragmentos. ¿Cuál es más rápido y por qué?

value = 5; if (condition) { value = 6; }

y:

if (condition) { value = 6; } else { value = 5; }

¿Qué pasa si el value es una matriz?

Nota: Sé que value = condition ? 6 : 5; value = condition ? 6 : 5; existe y espero que sea más rápido, pero no era una opción.

Editar (solicitado por el personal ya que la pregunta está en espera en este momento):

  • responda considerando el ensamblaje x86 generado por los compiladores principales (por ejemplo, g ++, clang ++, vc, mingw ) en versiones optimizadas y no optimizadas o el ensamblaje MIPS .
  • cuando el ensamblaje difiere, explique por qué una versión es más rápida y cuándo ( por ejemplo, "mejor porque no hay ramificación y la ramificación tiene el siguiente problema blahblah" )

¿Qué te haría pensar que alguno de ellos, incluso el revestimiento es más rápido o más lento?

unsigned int fun0 ( unsigned int condition, unsigned int value ) { value = 5; if (condition) { value = 6; } return(value); } unsigned int fun1 ( unsigned int condition, unsigned int value ) { if (condition) { value = 6; } else { value = 5; } return(value); } unsigned int fun2 ( unsigned int condition, unsigned int value ) { value = condition ? 6 : 5; return(value); }

Más líneas de código de un lenguaje de alto nivel le dan al compilador más para trabajar, así que si desea hacer una regla general al respecto, déle al compilador más código para trabajar. Si el algoritmo es el mismo que en los casos anteriores, uno esperaría que el compilador con una optimización mínima lo descubriera.

00000000 <fun0>: 0: e3500000 cmp r0, #0 4: 03a00005 moveq r0, #5 8: 13a00006 movne r0, #6 c: e12fff1e bx lr 00000010 <fun1>: 10: e3500000 cmp r0, #0 14: 13a00006 movne r0, #6 18: 03a00005 moveq r0, #5 1c: e12fff1e bx lr 00000020 <fun2>: 20: e3500000 cmp r0, #0 24: 13a00006 movne r0, #6 28: 03a00005 moveq r0, #5 2c: e12fff1e bx lr

No es una gran sorpresa que hiciera la primera función en un orden diferente, aunque el mismo tiempo de ejecución.

0000000000000000 <fun0>: 0: 7100001f cmp w0, #0x0 4: 1a9f07e0 cset w0, ne 8: 11001400 add w0, w0, #0x5 c: d65f03c0 ret 0000000000000010 <fun1>: 10: 7100001f cmp w0, #0x0 14: 1a9f07e0 cset w0, ne 18: 11001400 add w0, w0, #0x5 1c: d65f03c0 ret 0000000000000020 <fun2>: 20: 7100001f cmp w0, #0x0 24: 1a9f07e0 cset w0, ne 28: 11001400 add w0, w0, #0x5 2c: d65f03c0 ret

Esperemos que tenga la idea de que podría haber intentado esto si no fuera obvio que las diferentes implementaciones no eran realmente diferentes.

En cuanto a una matriz, no estoy seguro de cómo importa eso,

if(condition) { big blob of code a } else { big blob of code b }

solo voy a poner el mismo contenedor if-then-else alrededor de los grandes bloques de código, ya que su valor es = 5 o algo más complicado. Del mismo modo, la comparación, incluso si se trata de una gran cantidad de código, todavía tiene que calcularse, y a menudo se compila igual o no igual a algo negativo, si (condición) hacer algo a menudo se compila como si no fuera condición goto.

00000000 <fun0>: 0: 0f 93 tst r15 2: 03 24 jz $+8 ;abs 0xa 4: 3f 40 06 00 mov #6, r15 ;#0x0006 8: 30 41 ret a: 3f 40 05 00 mov #5, r15 ;#0x0005 e: 30 41 ret 00000010 <fun1>: 10: 0f 93 tst r15 12: 03 20 jnz $+8 ;abs 0x1a 14: 3f 40 05 00 mov #5, r15 ;#0x0005 18: 30 41 ret 1a: 3f 40 06 00 mov #6, r15 ;#0x0006 1e: 30 41 ret 00000020 <fun2>: 20: 0f 93 tst r15 22: 03 20 jnz $+8 ;abs 0x2a 24: 3f 40 05 00 mov #5, r15 ;#0x0005 28: 30 41 ret 2a: 3f 40 06 00 mov #6, r15 ;#0x0006 2e: 30 41

Acabamos de realizar este ejercicio con alguien más recientemente en . este compilador de mips curiosamente en ese caso no solo se dio cuenta de que las funciones eran las mismas, sino que una función simplemente saltó a la otra para ahorrar espacio en el código. Aunque no hice eso aquí

00000000 <fun0>: 0: 0004102b sltu $2,$0,$4 4: 03e00008 jr $31 8: 24420005 addiu $2,$2,5 0000000c <fun1>: c: 0004102b sltu $2,$0,$4 10: 03e00008 jr $31 14: 24420005 addiu $2,$2,5 00000018 <fun2>: 18: 0004102b sltu $2,$0,$4 1c: 03e00008 jr $31 20: 24420005 addiu $2,$2,5

Algunos objetivos más.

00000000 <_fun0>: 0: 1166 mov r5, -(sp) 2: 1185 mov sp, r5 4: 0bf5 0004 tst 4(r5) 8: 0304 beq 12 <_fun0+0x12> a: 15c0 0006 mov $6, r0 e: 1585 mov (sp)+, r5 10: 0087 rts pc 12: 15c0 0005 mov $5, r0 16: 1585 mov (sp)+, r5 18: 0087 rts pc 0000001a <_fun1>: 1a: 1166 mov r5, -(sp) 1c: 1185 mov sp, r5 1e: 0bf5 0004 tst 4(r5) 22: 0204 bne 2c <_fun1+0x12> 24: 15c0 0005 mov $5, r0 28: 1585 mov (sp)+, r5 2a: 0087 rts pc 2c: 15c0 0006 mov $6, r0 30: 1585 mov (sp)+, r5 32: 0087 rts pc 00000034 <_fun2>: 34: 1166 mov r5, -(sp) 36: 1185 mov sp, r5 38: 0bf5 0004 tst 4(r5) 3c: 0204 bne 46 <_fun2+0x12> 3e: 15c0 0005 mov $5, r0 42: 1585 mov (sp)+, r5 44: 0087 rts pc 46: 15c0 0006 mov $6, r0 4a: 1585 mov (sp)+, r5 4c: 0087 rts pc 00000000 <fun0>: 0: 00a03533 snez x10,x10 4: 0515 addi x10,x10,5 6: 8082 ret 00000008 <fun1>: 8: 00a03533 snez x10,x10 c: 0515 addi x10,x10,5 e: 8082 ret 00000010 <fun2>: 10: 00a03533 snez x10,x10 14: 0515 addi x10,x10,5 16: 8082 ret

y compiladores

con este código uno esperaría que los diferentes objetivos coincidan también

define i32 @fun0(i32 %condition, i32 %value) #0 { %1 = icmp ne i32 %condition, 0 %. = select i1 %1, i32 6, i32 5 ret i32 %. } ; Function Attrs: norecurse nounwind readnone define i32 @fun1(i32 %condition, i32 %value) #0 { %1 = icmp eq i32 %condition, 0 %. = select i1 %1, i32 5, i32 6 ret i32 %. } ; Function Attrs: norecurse nounwind readnone define i32 @fun2(i32 %condition, i32 %value) #0 { %1 = icmp ne i32 %condition, 0 %2 = select i1 %1, i32 6, i32 5 ret i32 %2 } 00000000 <fun0>: 0: e3a01005 mov r1, #5 4: e3500000 cmp r0, #0 8: 13a01006 movne r1, #6 c: e1a00001 mov r0, r1 10: e12fff1e bx lr 00000014 <fun1>: 14: e3a01006 mov r1, #6 18: e3500000 cmp r0, #0 1c: 03a01005 moveq r1, #5 20: e1a00001 mov r0, r1 24: e12fff1e bx lr 00000028 <fun2>: 28: e3a01005 mov r1, #5 2c: e3500000 cmp r0, #0 30: 13a01006 movne r1, #6 34: e1a00001 mov r0, r1 38: e12fff1e bx lr fun0: push.w r4 mov.w r1, r4 mov.w r15, r12 mov.w #6, r15 cmp.w #0, r12 jne .LBB0_2 mov.w #5, r15 .LBB0_2: pop.w r4 ret fun1: push.w r4 mov.w r1, r4 mov.w r15, r12 mov.w #5, r15 cmp.w #0, r12 jeq .LBB1_2 mov.w #6, r15 .LBB1_2: pop.w r4 ret fun2: push.w r4 mov.w r1, r4 mov.w r15, r12 mov.w #6, r15 cmp.w #0, r12 jne .LBB2_2 mov.w #5, r15 .LBB2_2: pop.w r4 ret

Ahora, técnicamente, hay una diferencia de rendimiento en algunas de estas soluciones, a veces el resultado es 5 casos tiene un salto sobre el resultado es 6 código, y viceversa, ¿es una rama más rápida que la ejecución? se podría discutir pero la ejecución debería variar. Pero eso es más una condición if versus if if no en el código, lo que hace que el compilador haga el if if jump over else ejecutar a través. pero esto no se debe necesariamente al estilo de codificación, sino a la comparación y los casos if y else en cualquier sintaxis.


En el código no optimizado, el primer ejemplo asigna una variable siempre una vez y a veces dos veces. El segundo ejemplo solo asigna una variable una vez. El condicional es el mismo en ambas rutas de código, por lo que eso no debería importar. En código optimizado, depende del compilador.

Como siempre, si le preocupa, genere el ensamblado y vea qué está haciendo realmente el compilador.


En lenguaje de seudoensamblaje,

li #0, r0 test r1 beq L1 li #1, r0 L1:

puede o no ser más rápido que

test r1 beq L1 li #1, r0 bra L2 L1: li #0, r0 L2:

dependiendo de lo sofisticada que sea la CPU real. Pasando de lo más simple a lo más elegante:

  • Con cualquier CPU fabricada después de aproximadamente 1990, el buen rendimiento depende del ajuste del código dentro del caché de instrucciones . En caso de duda, por lo tanto, minimice el tamaño del código. Esto pesa a favor del primer ejemplo.

  • Con una CPU básica " en orden, de cinco etapas ", que sigue siendo aproximadamente lo que obtienes en muchos microcontroladores, hay una burbuja de tubería cada vez que se toma una rama, condicional o incondicional, por lo que también es importante minimizar El número de instrucciones de rama. Esto también pesa a favor del primer ejemplo.

  • Las CPU algo más sofisticadas, lo suficientemente sofisticadas como para hacer una " ejecución fuera de orden ", pero no lo suficientemente sofisticadas como para usar las implementaciones más conocidas de ese concepto, pueden generar burbujas de canalización cada vez que se encuentran con riesgos de escritura tras escritura . Esto pesa a favor del segundo ejemplo, donde r0 se escribe solo una vez sin importar qué. Estas CPU suelen ser lo suficientemente sofisticadas como para procesar ramas incondicionales en el buscador de instrucciones, por lo que no solo está intercambiando la penalización de escritura tras escritura por una penalización de rama.

    No sé si alguien todavía está haciendo este tipo de CPU. Sin embargo, es probable que las CPU que utilizan las "implementaciones más conocidas" de ejecución fuera de orden acorten las instrucciones que se usan con menos frecuencia, por lo que debe tener en cuenta que este tipo de cosas pueden suceder. Un ejemplo real son las dependencias de datos falsos en los registros de destino en popcnt y lzcnt en las CPU Sandy Bridge .

  • En el extremo más alto, el motor OOO terminará emitiendo exactamente la misma secuencia de operaciones internas para ambos fragmentos de código: esta es la versión de hardware de "no se preocupe, el compilador generará el mismo código de máquina de cualquier manera". Sin embargo, el tamaño del código aún importa, y ahora también debería preocuparse por la previsibilidad de la rama condicional. Las fallas de predicción de rama potencialmente causan una descarga completa de la tubería, lo cual es catastrófico para el rendimiento; ver .com/questions/11227809/… para entender cuánta diferencia puede hacer esto.

    Si la rama es altamente impredecible y su CPU tiene instrucciones condicional o de movimiento condicional, este es el momento de usarlas:

    li #0, r0 test r1 setne r0

    o

    li #0, r0 li #1, r2 test r1 movne r2, r0

    La versión de conjunto condicional también es más compacta que cualquier otra alternativa; Si esa instrucción está disponible, prácticamente se garantiza que es lo correcto para este escenario, incluso si la rama era predecible. La versión de movimiento condicional requiere un registro de memoria virtual adicional, y siempre desperdicia el valor de una instrucción de despachar y ejecutar recursos; Si la rama era de hecho predecible, la versión ramificada bien podría ser más rápida.


La respuesta de CompuChip muestra que para int ambos están optimizados para el mismo ensamblaje, por lo que no importa.

¿Qué pasa si el valor es una matriz?

Interpretaré esto de una manera más general, es decir, qué pasa si el value es de un tipo cuyas construcciones y tareas son caras (y los movimientos son baratos).

entonces

T value = init1; if (condition) value = init2;

es subóptimo porque en caso de que la condition sea ​​verdadera, realiza la inicialización innecesaria de init1 y luego realiza la asignación de la copia.

T value; if (condition) value = init2; else value = init3;

Este es mejor. Pero sigue siendo subóptimo si la construcción predeterminada es costosa y si la construcción de la copia es más costosa que la inicialización.

Tiene la solución de operador condicional que es buena:

T value = condition ? init1 : init2;

O, si no le gusta el operador condicional, puede crear una función auxiliar como esta:

T create(bool condition) { if (condition) return {init1}; else return {init2}; } T value = create(condition);

Dependiendo de qué init1 e init2 sean, también puede considerar esto:

auto final_init = condition ? init1 : init2; T value = final_init;

Pero nuevamente debo enfatizar que esto es relevante solo cuando la construcción y las tareas son realmente costosas para el tipo dado. E incluso entonces, solo al perfilar lo sabes con seguridad.


Ok, dado que el ensamblaje es una de las etiquetas, simplemente asumiré que su código es un pseudocódigo (y no necesariamente c) y lo traduciré por humanos en el ensamblaje 6502.

Primera opción (sin más)

ldy #$00 lda #$05 dey bmi false lda #$06 false brk

2da opción (con más)

ldy #$00 dey bmi else lda #$06 sec bcs end else lda #$05 end brk

Suposiciones: la condición está en el registro Y, establezca esto en 0 o 1 en la primera línea de cualquiera de las opciones, el resultado estará en el acumulador.

Entonces, después de contar ciclos para ambas posibilidades de cada caso, vemos que la primera construcción es generalmente más rápida; 9 ciclos cuando la condición es 0 y 10 ciclos cuando la condición es 1, mientras que la opción dos también es 9 ciclos cuando la condición es 0, pero 13 ciclos cuando la condición es 1. (los recuentos de ciclos no incluyen el BRK al final ).

Conclusión: If only es más rápido que la construcción If-Else .

Y para completar, aquí hay un value = condition + 5 optimizado value = condition + 5 solución value = condition + 5 :

ldy #$00 lda #$00 tya adc #$05 brk

Esto reduce nuestro tiempo a 8 ciclos ( nuevamente sin incluir el BRK al final ).


TL; DR: en el código no optimizado, if sin else parece irrelevantemente más eficiente, pero incluso con el nivel más básico de optimización habilitado, el código se reescribe básicamente a value = condition + 5 .

Lo probé y generé el ensamblado para el siguiente código:

int ifonly(bool condition, int value) { value = 5; if (condition) { value = 6; } return value; } int ifelse(bool condition, int value) { if (condition) { value = 6; } else { value = 5; } return value; }

En gcc 6.3 con optimizaciones deshabilitadas ( -O0 ), la diferencia relevante es:

mov DWORD PTR [rbp-8], 5 cmp BYTE PTR [rbp-4], 0 je .L2 mov DWORD PTR [rbp-8], 6 .L2: mov eax, DWORD PTR [rbp-8]

por si ifonly , mientras que ifelse tiene

cmp BYTE PTR [rbp-4], 0 je .L5 mov DWORD PTR [rbp-8], 6 jmp .L6 .L5: mov DWORD PTR [rbp-8], 5 .L6: mov eax, DWORD PTR [rbp-8]

Este último parece un poco menos eficiente porque tiene un salto adicional, pero ambos tienen al menos dos y como máximo tres asignaciones, a menos que realmente necesite exprimir hasta la última gota de rendimiento (pista: a menos que esté trabajando en un transbordador espacial, no , e incluso entonces probablemente no) la diferencia no será notable.

Sin embargo, incluso con el nivel de optimización más bajo ( -O1 ) ambas funciones se reducen al mismo:

test dil, dil setne al movzx eax, al add eax, 5

que es básicamente el equivalente de

return 5 + condition;

suponiendo que la condition sea ​​cero o uno. Los niveles de optimización más altos realmente no cambian la salida, excepto que logran evitar el movzx al poner a cero de manera eficiente el registro EAX al comienzo.

Descargo de responsabilidad: probablemente no debería escribir la 5 + condition usted mismo (aunque el estándar garantiza que la conversión de true a un tipo entero da 1 ) porque su intención puede no ser inmediatamente obvia para las personas que leen su código (que puede incluir su futuro yo). El objetivo de este código es mostrar que lo que produce el compilador en ambos casos es (prácticamente) idéntico. Ciprian Tomoiaga lo dice bastante bien en los comentarios:

el trabajo de un humano es escribir código para humanos y dejar que el compilador escriba código para la máquina .