La siguiente implementación de square produce una serie de sentencias cmp / je como esperaría de una sentencia if encadenada:
int square(int num) {
if (num == 0){
return 0;
} else if (num == 1){
return 1;
} else if (num == 2){
return 4;
} else if (num == 3){
return 9;
} else if (num == 4){
return 16;
} else if (num == 5){
return 25;
} else if (num == 6){
return 36;
} else if (num == 7){
return 49;
} else {
return num * num;
}
}
Y lo siguiente produce una tabla de datos para el retorno:
int square_2(int num) {
switch (num){
case 0: return 0;
case 1: return 1;
case 2: return 4;
case 3: return 9;
case 4: return 16;
case 5: return 25;
case 6: return 36;
case 7: return 49;
default: return num * num;
}
}
¿Por qué gcc no puede optimizar el superior en el inferior?
Desmontaje para referencia: https://godbolt.org/z/UP_igi
EDITAR: curiosamente, MSVC genera una tabla de salto en lugar de una tabla de datos para el caso de cambio. Y sorprendentemente, el sonido metálico los optimiza para el mismo resultado.
c++
c
gcc
optimization
compiler-optimization
chacham15
fuente
fuente
return
s; los casos no tienenbreaks
, por lo que el interruptor también tiene un orden específico de ejecución. La cadena if / else tiene retornos en cada rama, la semántica en este caso es equivalente. La optimización no es imposible . Como contraejemplo, icc no optimiza ninguna de las funciones.Respuestas:
El código generado para
switch-case
convencionalmente utiliza una tabla de salto. En este caso, el retorno directo a través de una tabla de búsqueda parece ser una optimización haciendo uso del hecho de que cada caso aquí implica un retorno. Aunque el estándar no garantiza ese efecto, me sorprendería que un compilador generara una serie de comparaciones en lugar de una tabla de salto para una caja de conmutación convencional.Ahora llegando a
if-else
, es exactamente lo contrario. Si bien seswitch-case
ejecuta en tiempo constante, independientemente del número de ramas,if-else
está optimizado para un número menor de ramas. Aquí, esperaría que el compilador generara básicamente una serie de comparaciones en el orden en que las escribió.Así que si yo había usado
if-else
porque espero que la mayoría de las llamadas asquare()
ser de0
o1
y rara vez para otros valores, a continuación, 'optimizar' esta a una tabla de operaciones de búsqueda en realidad podría causar mi código para correr más lento de lo esperado, venciendo mi propósito para el uso de unif
lugar de aswitch
. Por lo tanto, aunque es discutible, creo que GCC está haciendo lo correcto y el ruido metálico está siendo demasiado agresivo en su optimización.Alguien, en los comentarios, compartió un enlace donde clang realiza esta optimización y también genera código basado en tablas de búsqueda
if-else
. Algo notable sucede cuando reducimos el número de casos a solo dos (y por defecto) con el sonido metálico. Una vez más genera un código idéntico para if y switch, pero esta vez, cambia para comparar y se mueve en lugar del enfoque de tabla de búsqueda, para ambos. ¡Esto significa que incluso el sonido metálico que favorece el cambio sabe que el patrón 'si' es más óptimo cuando el número de casos es pequeño!En resumen, una secuencia de comparaciones
if-else
y una tabla de saltoswitch-case
es el patrón estándar que los compiladores tienden a seguir y los desarrolladores tienden a esperar cuando escriben código. Sin embargo, para ciertos casos especiales, algunos compiladores pueden optar por romper este patrón donde creen que proporciona una mejor optimización. Otros compiladores podrían elegir seguir el patrón de todos modos, incluso si aparentemente no son óptimos, confiando en que el desarrollador sepa lo que quiere. Ambos son enfoques válidos con sus propias ventajas y desventajas.fuente
0
y1
)?if
es obviamente más rápido? Ahora, aquí hay un ejemplo de una plataforma donde 0 y 1 serían más rápidos cuando se usaif
que cuando se usa el interruptor: godbolt.org/z/wcJhvS ( tenga en cuenta que también hay muchas otras optimizaciones en juego aquí)if
declaraciones explícitas o automáticamente por el compilador. No soy un experto en ARM, por lo que no estoy realmente seguro de si la afirmación que hace sobreswitch
ser más rápido de lo queif
es cierto. Dependería de la penalización por sucursales mal predichas, y eso realmente dependería de qué ARM.Una posible razón es que si los valores bajos de
num
son más probables, por ejemplo siempre 0, el código generado para el primero podría ser más rápido. El código generado para el interruptor tarda el mismo tiempo para todos los valores.Comparando los mejores casos, de acuerdo con esta tabla . Vea esta respuesta para la explicación de la tabla.
Si
num == 0
, para "si" tiene xor, prueba, je (con salto), ret. Latencia: 1 + 1 + salto. Sin embargo, xor y test son independientes, por lo que la velocidad de ejecución real sería más rápida que 1 + 1 ciclos.Si
num < 7
, para "cambiar" tienes mov, cmp, ja (sin salto), mov, ret. Latencia: 2 + 1 + sin salto + 2.Una instrucción de salto que no resulta en salto es más rápida que una que resulta en salto. Sin embargo, la tabla no define la latencia para un salto, por lo que no me queda claro cuál es mejor. Es posible que el último siempre sea mejor y GCC simplemente no pueda optimizarlo.
fuente