¿’Cambiar’ es más rápido que ‘si’?

¿Es una instrucción switch realmente más rápida que una instrucción if ?

Ejecuté el siguiente código en el comstackdor x64 C ++ de Visual Studio 2010 con el indicador /Ox :

 #include  #include  #include  #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:

Instrucción de conmutación: 5261 ms
Si enunciado: 5196 ms

Por lo que he aprendido, las declaraciones de switch aparentemente usan tablas de salto para optimizar la ramificación.

Preguntas:

  1. ¿Cómo sería una tabla de salto básica, en x86 o x64?

  2. ¿Este código está usando una tabla de salto?

  3. ¿Por qué no hay diferencia de rendimiento en este ejemplo? ¿Hay alguna situación en la que haya una diferencia de rendimiento significativa?


Desassembly 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:

Interesantes resultados aquí . Sin embargo, no estoy seguro de por qué uno es más rápido y uno es más lento.

Hay varias optimizaciones que un comstackdor puede hacer en un switch. No creo que la mencionada “tabla de salto” sea muy útil, ya que solo funciona cuando la entrada puede estar limitada de alguna manera.

C El pseudocódigo para una “tabla de salto” sería algo como esto : tenga en cuenta que el comstackdor en la práctica necesitaría insertar alguna forma de prueba si alrededor de la tabla para asegurarse de que la entrada sea válida en la tabla. Tenga en cuenta también que solo funciona en el caso específico de que la entrada sea una corrida de números consecutivos.

Si el número de twigs en un switch es extremadamente grande, un comstackdor puede hacer cosas como usar la búsqueda binaria en los valores del switch, 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 un interruptor es, y no da como resultado un mayor tamaño de código generado. Pero para ver eso, su código de prueba necesitaría MUCHAS ramificaciones más para ver cualquier diferencia.

Para responder a sus preguntas específicas:

  1. Clang genera uno que se ve así:

     test_switch(char): # @test_switch(char) movl %edi, %eax cmpl $19, %edi jbe .LBB0_1 retq .LBB0_1: jmpq *.LJTI0_0(,%rax,8) jmp void call<0u>() # TAILCALL jmp void call<1u>() # TAILCALL jmp void call<2u>() # TAILCALL jmp void call<3u>() # TAILCALL jmp void call<4u>() # TAILCALL jmp void call<5u>() # TAILCALL jmp void call<6u>() # TAILCALL jmp void call<7u>() # TAILCALL jmp void call<8u>() # TAILCALL jmp void call<9u>() # TAILCALL jmp void call<10u>() # TAILCALL jmp void call<11u>() # TAILCALL jmp void call<12u>() # TAILCALL jmp void call<13u>() # TAILCALL jmp void call<14u>() # TAILCALL jmp void call<15u>() # TAILCALL jmp void call<16u>() # TAILCALL jmp void call<17u>() # TAILCALL jmp void call<18u>() # TAILCALL jmp void call<19u>() # TAILCALL .LJTI0_0: .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10 .quad .LBB0_11 .quad .LBB0_12 .quad .LBB0_13 .quad .LBB0_14 .quad .LBB0_15 .quad .LBB0_16 .quad .LBB0_17 .quad .LBB0_18 .quad .LBB0_19 .quad .LBB0_20 .quad .LBB0_21 
  2. Puedo decir que no está utilizando una tabla de salto – 4 instrucciones de comparación son claramente visibles:

     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) 

    Una solución basada en una tabla de salto no usa comparación en absoluto.

  3. O no hay suficientes twigs para hacer que el comstackdor genere una tabla de salto, o su comstackdor simplemente no las genera. No estoy seguro de qué.

EDIT 2014 : Ha habido una discusión en otros lugares de personas familiarizadas con el optimizador de LLVM diciendo 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 antes en 2011: con demasiada frecuencia veo a la gente pensando “si lo cambio, será el mismo momento sin importar cuántos casos tenga”, y eso es completamente falso. Incluso con una tabla de saltos, obtiene el costo de salto indirecto y paga las entradas en la tabla para cada caso; y el ancho de banda de la memoria es una gran ventaja en el hardware moderno.

Escribir el código para la legibilidad. Cualquier comstackdor digno de su sal va a ver una escalera if / else si y la transformará en un interruptor equivalente o viceversa si fuera más rápido hacerlo.

Para su pregunta:

1. ¿Cómo sería una tabla básica de salto, en x86 o x64?

La tabla de salto es la dirección de memoria que mantiene el puntero a las tags en algo así como la estructura de la matriz. El siguiente ejemplo te ayudará a entender cómo se ve la tabla de salto

 00B14538 D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 Ø.«.Ø.«.Ø.«.Ø.«. 00B14548 D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 00 00 00 00 Ø.«.Ø.«.Ø.«..... 00B14558 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00B14568 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 

enter image description here

Donde 00B14538 es el puntero a la tabla Jump, y el valor como D8 09 AB 00 representa el puntero de la 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 de verificación muy larga, en ese caso usar la tabla de salto reduce el rendimiento alcanzado, pero eso viene con el costo de la memoria.

Lema: el comstackdor es lo suficientemente inteligente como para manejar este caso 🙂

El comstackdor puede comstackr la sentencia switch como un código que es equivalente a if-statement, o para crear una tabla de salto. Es probable que elija uno sobre el otro en función de lo que ejecutará más rápido o generará el código más pequeño de alguna manera dependiendo de lo que haya especificado en las opciones del comstackdor; en el peor de los casos, tendrá la misma velocidad que las sentencias if.

Confiaría en que el comstackdor haga la mejor elección y se concentre en lo que hace que el código sea más legible.

Si el número de casos es muy grande, una tabla de saltos será mucho más rápida que una serie de si. Sin embargo, si los pasos entre los valores son muy grandes, la tabla de salto puede volverse grande y el comstackdor puede optar por no generar uno.

¿Cómo sabe que su computadora no estaba realizando ninguna tarea no relacionada con la prueba durante el ciclo de prueba de cambio y realizando menos tareas durante el ciclo de prueba si? Los resultados de su prueba no muestran nada como:

  1. la diferencia es muy pequeña
  2. solo hay un resultado, no una serie de resultados
  3. hay muy pocos casos

Mis resultados:

Yo agregué:

 printf("counter: %u\n", counter); 

hasta el final para que no optimice el bucle ya que el contador nunca se usó en su ejemplo, entonces ¿por qué el comstackdor realizaría el ciclo? Inmediatamente, el cambio siempre fue exitoso incluso con una micro-referencia.

El otro problema con tu código es:

 switch (counter % 4 + 1) 

en su ciclo de conmutación, versus

 const size_t c = counter % 4 + 1; 

en tu ciclo if Muy gran diferencia si arreglas eso. Creo que al poner la statement dentro de la statement del interruptor provoca que el comstackdor envíe el valor directamente a los registros de la CPU en lugar de ponerlo primero en la stack. Por lo tanto, esto es a favor de la statement de cambio y no una prueba equilibrada.

Ah, y creo que también deberías restablecer el contador entre pruebas. De hecho, es probable que deba utilizar algún tipo de número aleatorio en lugar de +1, +2, +3, etc., ya que probablemente optimice algo allí. Por número aleatorio, me refiero a un número basado en la hora actual, por ejemplo. De lo contrario, el comstackdor podría convertir ambas funciones en una operación matemática larga y ni siquiera molestarse con ningún bucle.

He modificado el código de Ryan lo suficiente para asegurarme de que el comstackdor no pudo resolver las cosas antes de que se ejecutara el código:

 #include  #include  #include  #define MAX_COUNT (1 << 26) size_t counter = 0; long long testSwitch() { clock_t start = clock(); size_t i; for (i = 0; i < MAX_COUNT; i++) { const size_t c = rand() % 20 + 1; switch (c) { case 1: counter += 20; break; case 2: counter += 33; break; case 3: counter += 62; break; case 4: counter += 15; break; case 5: counter += 416; break; case 6: counter += 3545; break; case 7: counter += 23; break; case 8: counter += 81; break; case 9: counter += 256; break; case 10: counter += 15865; break; case 11: counter += 3234; break; case 12: counter += 22345; break; case 13: counter += 1242; break; case 14: counter += 12341; break; case 15: counter += 41; break; case 16: counter += 34321; break; case 17: counter += 232; break; case 18: counter += 144231; break; case 19: counter += 32; break; case 20: counter += 1231; break; } } return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC; } long long testIf() { clock_t start = clock(); size_t i; for (i = 0; i < MAX_COUNT; i++) { const size_t c = rand() % 20 + 1; if (c == 1) { counter += 20; } else if (c == 2) { counter += 33; } else if (c == 3) { counter += 62; } else if (c == 4) { counter += 15; } else if (c == 5) { counter += 416; } else if (c == 6) { counter += 3545; } else if (c == 7) { counter += 23; } else if (c == 8) { counter += 81; } else if (c == 9) { counter += 256; } else if (c == 10) { counter += 15865; } else if (c == 11) { counter += 3234; } else if (c == 12) { counter += 22345; } else if (c == 13) { counter += 1242; } else if (c == 14) { counter += 12341; } else if (c == 15) { counter += 41; } else if (c == 16) { counter += 34321; } else if (c == 17) { counter += 232; } else if (c == 18) { counter += 144231; } else if (c == 19) { counter += 32; } else if (c == 20) { counter += 1231; } } return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC; } int main() { srand(time(NULL)); printf("Starting...\n"); printf("Switch statement: %lld ms\n", testSwitch()); fflush(stdout); printf("counter: %d\n", counter); counter = 0; srand(time(NULL)); printf("If statement: %lld ms\n", testIf()); fflush(stdout); printf("counter: %d\n", counter); } 

interruptor: 3740
si: 3980

(resultados similares en múltiples bashs)

También reduje el número de casos / si a 5 y la función de cambio aún ganó.

Un buen comstackdor de optimización como MSVC puede generar:

  1. una mesa de salto simple si las cajas están dispuestas en un largo rango agradable
  2. una tabla de saltos dispersa (dos niveles) si hay muchas lagunas
  3. una serie de ifs si el número de casos es pequeño o los valores no están muy juntos
  4. una combinación de arriba si los casos representan varios grupos de rangos estrechamente espaciados.

En resumen, si el switch parece ser más lento que una serie de ifs, el comstackdor podría simplemente convertirlo en uno. Y es probable que no sea solo una secuencia de comparaciones para cada caso, sino un árbol de búsqueda binario. Vea aquí para un ejemplo.

Voy a responder 2) y hacer 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 de la tabla. Una tabla de salto tendría más sentido cuando hay muchos posibles destinos de cambio. Tal vez el optimizador sabe que la lógica simple si no es más rápida a menos que el número de destinos sea mayor que algún umbral. Pruebe su ejemplo de nuevo con decir 20 posibilidades en lugar de 4.

Estaba intrigado y eché un vistazo a lo que podría cambiar sobre su ejemplo para que ejecute la statement 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 lento 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 .

No, estos son los siguientes si saltas en otro modo si luego saltas en otro … Una tabla de salto tendría una tabla de direcciones o usar un hash o algo así.

Más rápido o más lento es subjetivo. Por ejemplo, podría hacer que el caso 1 sea lo último en lugar de primero y si el progtwig de prueba o el progtwig del mundo real utilizara el caso 1 la mayor parte del tiempo, el código sería más lento con esta implementación. Así que solo reorganizar la lista de casos, dependiendo de la implementación, puede marcar una gran diferencia.

Si hubiera utilizado los casos 0-3 en lugar de 1-4, el comstackdor podría haber usado una tabla de salto, el comstackdor debería haber resuelto eliminar su +1 de todos modos. Tal vez fue la pequeña cantidad de artículos. Si lo hubiera hecho 0 – 15 o 0 – 31, por ejemplo, puede haberlo implementado con una tabla o haber usado algún otro atajo. El comstackdor 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 comstackción y diferencias de versión y diferencias de optimización. Si quieres una mesa de saltos, crea una tabla de saltos, si quieres un árbol de if-then-else crea un árbol if-then-else. Si desea que el comstackdor decida, use una instrucción switch / case.

Sin embargo, no estoy seguro de por qué uno es más rápido y uno es más lento.

En realidad, no es demasiado difícil de explicar … Si recuerda que las twigs mal predichas son de decenas a cientos de veces más caras que las twigs pronosticadas correctamente.

En la versión % 20 , el primer caso / si es siempre el que golpea. Las CPU modernas “aprenden” qué twigs se suelen tomar y cuáles no, por lo que pueden predecir fácilmente cómo se comportará esta twig en casi cada iteración del ciclo. Eso explica por qué vuela la versión “si”; nunca tiene que ejecutar nada más allá de la primera prueba, y (correctamente) predice el resultado de esa prueba para la mayoría de las iteraciones. Obviamente, el “interruptor” se implementa de forma ligeramente diferente, tal vez incluso una tabla de salto, que puede ser lenta gracias a la twig calculada.

En la versión % 21 , las twigs son esencialmente aleatorias. Por lo tanto, no solo muchos de ellos ejecutan cada iteración, la CPU no puede adivinar hacia dónde irán. Este es el caso donde es probable que una tabla de salto (u otra optimización de “cambio”) ayude.

Es muy difícil predecir cómo se comportará una pieza de código con un comstackdor y una CPU modernos, y se vuelve más difícil con cada generación. El mejor consejo es “ni siquiera te molestes en intentar; siempre en el perfil”. Ese consejo mejora, y el conjunto de personas que pueden ignorarlo se vuelve cada vez más pequeño, cada año.

Todo lo cual es para decir que mi explicación anterior es en gran parte una suposición. 🙂

Tenga en cuenta que cuando un interruptor NO se comstack en una tabla de salto, a menudo puede escribir si es más eficiente que el interruptor …

(1) si los casos tienen un pedido, en lugar de las peores pruebas para todas las N, puede escribir sus if para probar si está en la mitad superior o inferior, luego en cada mitad de ese estilo de búsqueda binaria … lo que da como resultado 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 su if para aislar esos casos primero puede acelerar el tiempo promedio a través de

Aquí hay algunos resultados del antiguo (ahora difícil de encontrar) benchmark bench ++:

 Test Name: F000003 Class Name: Style CPU Time: 0.781 nanoseconds plus or minus 0.0715 Wall/CPU: 1.00 ratio. Iteration Count: 1677721600 Test Description: Time to test a global using a 2-way if/else if statement compare this test with F000004 Test Name: F000004 Class Name: Style CPU Time: 1.53 nanoseconds plus or minus 0.0767 Wall/CPU: 1.00 ratio. Iteration Count: 1677721600 Test Description: Time to test a global using a 2-way switch statement compare this test with F000003 Test Name: F000005 Class Name: Style CPU Time: 7.70 nanoseconds plus or minus 0.385 Wall/CPU: 1.00 ratio. Iteration Count: 1677721600 Test Description: Time to test a global using a 10-way if/else if statement compare this test with F000006 Test Name: F000006 Class Name: Style CPU Time: 2.00 nanoseconds plus or minus 0.0999 Wall/CPU: 1.00 ratio. Iteration Count: 1677721600 Test Description: Time to test a global using a 10-way switch statement compare this test with F000005 Test Name: F000007 Class Name: Style CPU Time: 3.41 nanoseconds plus or minus 0.171 Wall/CPU: 1.00 ratio. Iteration Count: 1677721600 Test Description: Time to test a global using a 10-way sparse switch statement compare this test with F000005 and F000006 

Lo que podemos ver a partir de esto es que (en esta máquina, con este comstackdor – VC ++ 9.0 x64), cada prueba if tarda unos 0,7 nanosegundos. A medida que aumenta el número de pruebas, el tiempo se escala casi perfectamente linealmente.

Con la instrucción de cambio, casi no hay diferencia en la velocidad entre una prueba de 2 vías y una de 10, siempre que los valores sean densos. La prueba de 10 vías con valores escasos requiere aproximadamente 1,6 veces más tiempo que la prueba de 10 vías con valores densos, pero incluso con valores dispersos, es mejor que el doble de la velocidad de un if / else if 10 vías.

En pocas palabras: usar solo una prueba de 4 vías realmente no le mostrará mucho sobre el rendimiento de switch vs if / else . Si nos fijamos en 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 un if / else , ~ 2.0 para el switch )

Ninguna. En la mayoría de los casos particulares en los que ingresa al ensamblador y realiza mediciones reales del rendimiento, su pregunta es simplemente la incorrecta. Para el ejemplo dado, su forma de pensar es demasiado corta desde

 counter += (4 - counter % 4); 

looks to me to be the correct increment expression that you should be using.