¿Es una switch
declaración realmente más rápida que una if
declaración?
Ejecuté el siguiente código en el compilador x64 C ++ de Visual Studio 2010 con la /Ox
bandera:
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#define MAX_COUNT (1 << 29)
size_t counter = 0;
size_t testSwitch()
{
clock_t start = clock();
size_t i;
for (i = 0; i < MAX_COUNT; i++)
{
switch (counter % 4 + 1)
{
case 1: counter += 4; break;
case 2: counter += 3; break;
case 3: counter += 2; break;
case 4: counter += 1; break;
}
}
return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}
size_t testIf()
{
clock_t start = clock();
size_t i;
for (i = 0; i < MAX_COUNT; i++)
{
const size_t c = counter % 4 + 1;
if (c == 1) { counter += 4; }
else if (c == 2) { counter += 3; }
else if (c == 3) { counter += 2; }
else if (c == 4) { counter += 1; }
}
return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}
int main()
{
printf("Starting...\n");
printf("Switch statement: %u ms\n", testSwitch());
printf("If statement: %u ms\n", testIf());
}
y obtuve estos resultados:
Declaración de cambio: 5261 ms
Declaración de if: 5196 ms
Por lo que he aprendido, las switch
declaraciones aparentemente usan tablas de salto para optimizar la ramificación.
Preguntas:
¿Cómo sería una mesa de salto básica, en x86 o x64?
¿Este código está usando una tabla de salto?
¿Por qué no hay diferencia de rendimiento en este ejemplo? ¿Hay alguna situación en la que no es una diferencia significativa del rendimiento?
Desmontaje del código:
testIf:
13FE81B10 sub rsp,48h
13FE81B14 call qword ptr [__imp_clock (13FE81128h)]
13FE81B1A mov dword ptr [start],eax
13FE81B1E mov qword ptr [i],0
13FE81B27 jmp testIf+26h (13FE81B36h)
13FE81B29 mov rax,qword ptr [i]
13FE81B2E inc rax
13FE81B31 mov qword ptr [i],rax
13FE81B36 cmp qword ptr [i],20000000h
13FE81B3F jae testIf+0C3h (13FE81BD3h)
13FE81B45 xor edx,edx
13FE81B47 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B4E mov ecx,4
13FE81B53 div rax,rcx
13FE81B56 mov rax,rdx
13FE81B59 inc rax
13FE81B5C mov qword ptr [c],rax
13FE81B61 cmp qword ptr [c],1
13FE81B67 jne testIf+6Dh (13FE81B7Dh)
13FE81B69 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B70 add rax,4
13FE81B74 mov qword ptr [counter (13FE835D0h)],rax
13FE81B7B jmp testIf+0BEh (13FE81BCEh)
13FE81B7D cmp qword ptr [c],2
13FE81B83 jne testIf+89h (13FE81B99h)
13FE81B85 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B8C add rax,3
13FE81B90 mov qword ptr [counter (13FE835D0h)],rax
13FE81B97 jmp testIf+0BEh (13FE81BCEh)
13FE81B99 cmp qword ptr [c],3
13FE81B9F jne testIf+0A5h (13FE81BB5h)
13FE81BA1 mov rax,qword ptr [counter (13FE835D0h)]
13FE81BA8 add rax,2
13FE81BAC mov qword ptr [counter (13FE835D0h)],rax
13FE81BB3 jmp testIf+0BEh (13FE81BCEh)
13FE81BB5 cmp qword ptr [c],4
13FE81BBB jne testIf+0BEh (13FE81BCEh)
13FE81BBD mov rax,qword ptr [counter (13FE835D0h)]
13FE81BC4 inc rax
13FE81BC7 mov qword ptr [counter (13FE835D0h)],rax
13FE81BCE jmp testIf+19h (13FE81B29h)
13FE81BD3 call qword ptr [__imp_clock (13FE81128h)]
13FE81BD9 sub eax,dword ptr [start]
13FE81BDD imul eax,eax,3E8h
13FE81BE3 cdq
13FE81BE4 mov ecx,3E8h
13FE81BE9 idiv eax,ecx
13FE81BEB cdqe
13FE81BED add rsp,48h
13FE81BF1 ret
testSwitch:
13FE81C00 sub rsp,48h
13FE81C04 call qword ptr [__imp_clock (13FE81128h)]
13FE81C0A mov dword ptr [start],eax
13FE81C0E mov qword ptr [i],0
13FE81C17 jmp testSwitch+26h (13FE81C26h)
13FE81C19 mov rax,qword ptr [i]
13FE81C1E inc rax
13FE81C21 mov qword ptr [i],rax
13FE81C26 cmp qword ptr [i],20000000h
13FE81C2F jae testSwitch+0C5h (13FE81CC5h)
13FE81C35 xor edx,edx
13FE81C37 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C3E mov ecx,4
13FE81C43 div rax,rcx
13FE81C46 mov rax,rdx
13FE81C49 inc rax
13FE81C4C mov qword ptr [rsp+30h],rax
13FE81C51 cmp qword ptr [rsp+30h],1
13FE81C57 je testSwitch+73h (13FE81C73h)
13FE81C59 cmp qword ptr [rsp+30h],2
13FE81C5F je testSwitch+87h (13FE81C87h)
13FE81C61 cmp qword ptr [rsp+30h],3
13FE81C67 je testSwitch+9Bh (13FE81C9Bh)
13FE81C69 cmp qword ptr [rsp+30h],4
13FE81C6F je testSwitch+0AFh (13FE81CAFh)
13FE81C71 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C73 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C7A add rax,4
13FE81C7E mov qword ptr [counter (13FE835D0h)],rax
13FE81C85 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C87 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C8E add rax,3
13FE81C92 mov qword ptr [counter (13FE835D0h)],rax
13FE81C99 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C9B mov rax,qword ptr [counter (13FE835D0h)]
13FE81CA2 add rax,2
13FE81CA6 mov qword ptr [counter (13FE835D0h)],rax
13FE81CAD jmp testSwitch+0C0h (13FE81CC0h)
13FE81CAF mov rax,qword ptr [counter (13FE835D0h)]
13FE81CB6 inc rax
13FE81CB9 mov qword ptr [counter (13FE835D0h)],rax
13FE81CC0 jmp testSwitch+19h (13FE81C19h)
13FE81CC5 call qword ptr [__imp_clock (13FE81128h)]
13FE81CCB sub eax,dword ptr [start]
13FE81CCF imul eax,eax,3E8h
13FE81CD5 cdq
13FE81CD6 mov ecx,3E8h
13FE81CDB idiv eax,ecx
13FE81CDD cdqe
13FE81CDF add rsp,48h
13FE81CE3 ret
Actualizar:
Resultados interesantes aquí . Sin embargo, no estoy seguro de por qué uno es más rápido y uno es más lento.
c
performance
switch-statement
assembly
jump-table
usuario541686
fuente
fuente
5196 vs. 5261 shouldn't be enough to actually care
-> No estoy seguro de si entendió mal la pregunta o si mal entendí su comentario, pero ¿no es el objetivo de mi pregunta preguntar por qué no hay una diferencia? (¿Alguna vez pretendo que una diferencia significativa para preocuparse?)Respuestas:
Hay varias optimizaciones que un compilador puede hacer en un switch. Sin embargo, no creo que la "tabla de salto" mencionada con frecuencia sea muy útil, ya que solo funciona cuando la entrada se puede limitar de alguna manera.
C El seudocódigo para una "tabla de salto" sería algo como esto : tenga en cuenta que el compilador en la práctica necesitaría insertar algún tipo de prueba if en la tabla para asegurarse de que la entrada era válida en la tabla. Tenga en cuenta también que solo funciona en el caso específico de que la entrada sea una serie de números consecutivos.
Si el número de ramas en un conmutador es extremadamente grande, un compilador puede hacer cosas como usar la búsqueda binaria en los valores del conmutador, lo que (en mi opinión) sería una optimización mucho más útil, ya que aumenta significativamente el rendimiento en algunos escenarios, es tan general como lo es un switch, y no da como resultado un mayor tamaño de código generado. Pero para ver eso, su código de prueba necesitaría MUCHO más ramas para ver alguna diferencia.
Para responder a sus preguntas específicas:
Tañido genera uno que se parece a esto :
Puedo decir que no está usando una tabla de salto: 4 instrucciones de comparación son claramente visibles:
Una solución basada en la tabla de salto no usa comparación en absoluto.
EDITAR 2014 : ha habido alguna discusión en otras partes de personas familiarizadas con el optimizador LLVM que dicen que la optimización de la tabla de salto puede ser importante en muchos escenarios; por ejemplo, en casos donde hay una enumeración con muchos valores y muchos casos contra valores en dicha enumeración. Dicho esto, mantengo lo que dije anteriormente en 2011: con demasiada frecuencia veo personas que piensan "si hago un cambio, será al mismo tiempo, sin importar cuántos casos tenga", y eso es completamente falso. Incluso con una tabla de salto, obtienes el costo de salto indirecto y pagas las entradas en la tabla para cada caso; y el ancho de banda de la memoria es un gran problema en el hardware moderno.
Escribir código para facilitar la lectura. Cualquier compilador que valga la pena verá una escalera if / else if y la transformará en un interruptor equivalente o viceversa si sería más rápido hacerlo.
fuente
switch
salidas. Soren dijo varias otras cosas que quería decir después de leer esta respuesta.if
cláusulas ya se ha ajustado a mano para que coincida con las necesidades de frecuencia y rendimiento relativo, mientras que aswitch
se ve tradicionalmente como una invitación abierta para optimizar, sin embargo, el compilador elige. Buen punto para saltar más alláswitch
:-). El tamaño del código depende de los casos / rango, podría ser mejor. Finalmente, algunas enumeraciones, campos de bits ychar
escenarios son inherentemente válidos / acotados y sin gastos generales.A su pregunta:
1. ¿Cómo sería una mesa de salto básica, en x86 o x64?
La tabla de salto es una dirección de memoria que mantiene el puntero a las etiquetas en algo así como la estructura de la matriz. El siguiente ejemplo lo ayudará a comprender cómo se organizan las tablas de salto
Donde 00B14538 es el puntero a la tabla Jump, y un valor como D8 09 AB 00 representa el puntero de etiqueta.
2.¿Este código usa una tabla de salto? No en este caso
3. ¿Por qué no hay diferencia de rendimiento en este ejemplo?
No hay diferencia de rendimiento porque la instrucción para ambos casos se ve igual, no hay tabla de salto.
4. ¿Hay alguna situación en la que haya una diferencia de rendimiento significativa?
Si tiene una secuencia muy larga de verificación if , en ese caso el uso de una tabla de salto mejora el rendimiento (las instrucciones de ramificación / jmp son caras si no predicen casi a la perfección) pero conlleva el costo de la memoria.
El código para todas las instrucciones de comparación también tiene cierto tamaño, por lo que especialmente con punteros o compensaciones de 32 bits, una sola búsqueda en la tabla de salto podría no costar mucho más tamaño en un ejecutable.
Conclusión: el compilador es lo suficientemente inteligente como para manejar este caso y generar las instrucciones apropiadas :)
fuente
gcc -S
salida: una secuencia de entradas.long L1
/.long L2
tabla es más significativa que un hexdump, y más útil para alguien que quiere aprender a mirar el compilador. (Aunque supongo que solo mirarías el código del interruptor para ver si era un jmp indirecto o un montón de jcc).El compilador es libre de compilar la instrucción switch como un código que es equivalente a la instrucción if, o de crear una tabla de salto. Probablemente elegirá uno sobre el otro en función de lo que se ejecutará más rápido o generará el código más pequeño, dependiendo de lo que haya especificado en las opciones del compilador, por lo que, en el peor de los casos, será la misma velocidad que las declaraciones if
Confiaría en que el compilador haga la mejor elección y se centre en lo que hace que el código sea más legible.
Si el número de casos se vuelve muy grande, una tabla de salto será mucho más rápida que una serie de if. Sin embargo, si los pasos entre los valores son muy grandes, la tabla de salto puede llegar a ser grande y el compilador puede optar por no generar uno.
fuente
¿Cómo sabe que su computadora no realizaba alguna tarea no relacionada con la prueba durante el ciclo de prueba del interruptor y realizaba menos tareas durante el ciclo de prueba if? Los resultados de su prueba no muestran nada como:
Mis resultados:
Agregué:
hasta el final para que no optimice el bucle ya que el contador nunca se usó en su ejemplo, entonces ¿por qué el compilador realizaría el bucle? Inmediatamente, el cambio siempre estaba ganando incluso con un micro punto de referencia.
El otro problema con su código es:
en su ciclo de cambio, versus
en tu bucle if. Muy gran diferencia si arreglas eso. Creo que poner la declaración dentro de la declaración del interruptor provoca que el compilador envíe el valor directamente a los registros de la CPU en lugar de colocarlo primero en la pila. Por lo tanto, esto es a favor de la declaración de cambio y no una prueba equilibrada.
Ah, y creo que también deberías restablecer el contador entre pruebas. De hecho, probablemente debería usar algún tipo de número aleatorio en lugar de +1, +2, +3, etc., ya que probablemente optimizará algo allí. Por número aleatorio, me refiero a un número basado en la hora actual, por ejemplo. De lo contrario, el compilador podría convertir ambas funciones en una operación matemática larga y ni siquiera molestarse con ningún bucle.
Modifiqué el código de Ryan lo suficiente para asegurarme de que el compilador no pudiera resolver las cosas antes de que se ejecutara el código:
conmutador: 3740
si: 3980
(resultados similares en múltiples intentos)
También reduje el número de casos / ifs a 5 y la función de cambio aún ganó.
fuente
print
declaración? Lo agregué al final de todo el programa y no vi ninguna diferencia. Tampoco entiendo cuál es el "problema" con el otro ... ¿te importaría explicar cuál es la "gran diferencia"?Un buen compilador de optimización como MSVC puede generar:
En resumen, si el cambio parece ser más lento que una serie de ifs, el compilador podría convertirlo en uno. Y es probable que no sea solo una secuencia de comparaciones para cada caso, sino un árbol de búsqueda binario. Ver aquí para un ejemplo.
fuente
Contestaré 2) y haré algunos comentarios generales. 2) No, no hay una tabla de salto en el código de ensamblaje que ha publicado. Una tabla de salto es una tabla de destinos de salto, y una o dos instrucciones para saltar directamente a una ubicación indexada desde la tabla. Una mesa de salto tendría más sentido cuando hay muchos destinos de cambio posibles. Tal vez el optimizador sabe que la lógica es simple si no es más rápida a menos que el número de destinos sea mayor que algún umbral. Intente su ejemplo nuevamente con digamos 20 posibilidades en lugar de 4.
fuente
Estaba intrigado y eché un vistazo a lo que podría cambiar sobre su ejemplo para que ejecute la declaración de cambio más rápido.
Si llega a 40 sentencias if y agrega un caso 0, entonces el bloque if se ejecutará más lentamente que la sentencia switch equivalente. Tengo los resultados aquí: https://www.ideone.com/KZeCz .
El efecto de eliminar el caso 0 se puede ver aquí: https://www.ideone.com/LFnrX .
fuente
Estos son algunos resultados del antiguo benchmark bench ++ (ahora difícil de encontrar):
Lo que podemos ver de esto es que (en esta máquina, con este compilador - VC ++ 9.0 x64), cada
if
prueba toma aproximadamente 0.7 nanosegundos. A medida que aumenta el número de pruebas, el tiempo se escala casi perfectamente linealmente.Con la declaración de cambio, casi no hay diferencia en la velocidad entre una prueba de 2 vías y una de 10 vías, siempre que los valores sean densos. La prueba de 10 vías con valores dispersos toma aproximadamente 1,6 veces más tiempo que la prueba de 10 vías con valores densos, pero incluso con valores dispersos, aún mejor que el doble de la velocidad de una de 10 vías
if
/else if
.En pocas palabras: usar solo una prueba de 4 vías realmente no le mostrará mucho sobre el rendimiento de
switch
vsif
/else
. Si observa los números de este código, es bastante fácil interpolar el hecho de que para una prueba de 4 vías, esperaríamos que los dos produzcan resultados bastante similares (~ 2.8 nanosegundos para unif
/else
, ~ 2.0 paraswitch
).fuente
if
/ en lugar deelse
dispersarlos, etc. No puedo encontrar lasbench++
fuentes después de 10 minutos googleando.Tenga en cuenta que cuando un interruptor NO se compila en una tabla de salto, a menudo puede escribir si es más eficiente que el interruptor ...
(1) si los casos tienen un orden, en lugar de la prueba del peor de los casos para todos los N, puede escribir sus if para probar si en la mitad superior o inferior, luego en cada mitad de ese estilo de búsqueda binaria ... lo que resulta en el peor de los casos es logN en lugar de N
(2) si ciertos casos / grupos son mucho más frecuentes que otros casos, entonces diseñar sus if para aislar esos casos primero puede acelerar el tiempo promedio
fuente
No, estos son si luego salta más si luego saltas más ... Una tabla de salto tendría una tabla de direcciones o usaría un hash o algo así.
Más rápido o más lento es subjetivo. Podría, por ejemplo, hacer que el caso 1 sea lo último en lugar de lo primero y si su programa de prueba o programa del mundo real usó el caso 1 la mayor parte del tiempo, el código sería más lento con esta implementación. Entonces, solo reorganizar la lista de casos, dependiendo de la implementación, puede hacer una gran diferencia.
Si había usado los casos 0-3 en lugar de 1-4, el compilador podría haber usado una tabla de salto, el compilador debería haber descubierto la eliminación de su +1 de todos modos. Quizás fue la pequeña cantidad de artículos. Si lo hubiera hecho 0 - 15 o 0 - 31, por ejemplo, podría haberlo implementado con una tabla o haber utilizado algún otro acceso directo. El compilador es libre de elegir cómo implementa las cosas siempre que cumpla con la funcionalidad del código fuente. Y esto entra en diferencias de compilador y diferencias de versión y diferencias de optimización. Si quieres una mesa de salto, haz una tabla de salto, si quieres un árbol if-then-else haz un árbol if-then-else. Si desea que el compilador decida, use una instrucción switch / case.
fuente
En realidad, eso no es demasiado difícil de explicar ... Si recuerdas que las ramas mal predichas son decenas a cientos de veces más caras que las ramas correctamente predichas.
En la
% 20
versión, el primer caso / if es siempre el que golpea. Las CPU modernas "aprenden" qué ramas se toman generalmente y cuáles no, por lo que pueden predecir fácilmente cómo se comportará esta rama en casi cada iteración del bucle. Eso explica por qué la versión "if" vuela; nunca tiene que ejecutar nada más allá de la primera prueba y predice (correctamente) el resultado de esa prueba para la mayoría de las iteraciones. Obviamente, el "interruptor" se implementa de manera ligeramente diferente, tal vez incluso una tabla de salto, que puede ser lenta gracias a la rama calculada.En la
% 21
versión, las ramas son esencialmente aleatorias. Entonces, no solo muchos de ellos ejecutan cada iteración, la CPU no puede adivinar en qué dirección irán. Este es el caso en el que una tabla de salto (u otra optimización de "interruptor") puede ayudar.Es muy difícil predecir cómo funcionará un código con un compilador y una CPU modernos, y se vuelve más difícil con cada generación. El mejor consejo es "no te molestes en intentarlo, siempre perfila". Ese consejo mejora, y el conjunto de personas que pueden ignorarlo con éxito se reduce cada año.
Todo lo cual es para decir que mi explicación anterior es en gran medida una suposición. :-)
fuente
Ninguna. En los casos más particulares en los que ingresa al ensamblador y realiza mediciones reales del rendimiento, su pregunta es simplemente la incorrecta. Para el ejemplo dado, su pensamiento es definitivamente demasiado corto ya que
me parece que es la expresión de incremento correcta que deberías usar.
fuente