¿Por qué un conmutador no está optimizado de la misma manera que encadenado, si no en c / c ++?

39

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.

chacham15
fuente
3
¿Qué quiere decir "comportamiento indefinido"? Siempre que el comportamiento observable sea el mismo, el compilador puede generar el código de ensamblaje / máquina que desee
bolov
2
@ user207421 ignorando el returns; los casos no tienen breaks, 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.
user1810087
99
Tal vez la respuesta más simple ... gcc simplemente no puede ver esta estructura y optimizarla (todavía).
user1810087
3
Estoy de acuerdo con @ user1810087. Simplemente encontró el límite actual del proceso de refinamiento del compilador. Un sub-sub-caso que actualmente no es reconocido como optimizable (por algunos compiladores). De hecho, no todas las cadenas if-if pueden optimizarse de esa manera, sino solo el subconjunto en el que la misma variable se prueba con valores constantes.
Roberto Caboni
1
El if-else tiene un orden de ejecución diferente, de arriba a abajo. Aún así, reemplazar el código con solo si las declaraciones no mejoraron el código de la máquina. El interruptor, por otro lado, no tiene un orden de ejecución predefinido y es esencialmente solo una gloriosa tabla de salto de goto. Dicho esto, un compilador puede razonar sobre el comportamiento observable aquí, por lo que la optimización deficiente de la versión if-else es bastante decepcionante.
Lundin

Respuestas:

29

El código generado para switch-caseconvencionalmente 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 se switch-caseejecuta en tiempo constante, independientemente del número de ramas, if-elseestá 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-elseporque espero que la mayoría de las llamadas a square()ser de 0o 1y 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 un iflugar de a switch. 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-elsey una tabla de salto switch-casees 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.

th33lf
fuente
2
Sí, la optimización es una espada de múltiples filos: lo que escriben, lo que quieren, lo que obtienen y a quién maldecimos por eso.
Deduplicador
1
"... luego 'optimizar' esto para una búsqueda de tabla en realidad haría que mi código se ejecute más lento de lo que esperaba ..." ¿Puede proporcionar una justificación para esto? ¿Por qué una tabla de salto alguna vez sería más lenta que dos posibles ramas condicionales (para verificar las entradas 0y 1)?
Cody Gray
@CodyGray Tengo que confesar que no llegué al nivel de los ciclos de conteo: simplemente tuve la sensación de que la carga de la memoria a través de un puntero podría tomar más ciclos que comparar y saltar, pero podría estar equivocado. Sin embargo, espero que esté de acuerdo conmigo en que incluso en este caso, al menos para '0', ¿ ifes 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 usa ifque cuando se usa el interruptor: godbolt.org/z/wcJhvS ( tenga en cuenta que también hay muchas otras optimizaciones en juego aquí)
33 de
1
Bueno, contar ciclos no funciona en las arquitecturas superescalares OOO modernas de todos modos. :-) Las cargas de memoria no van a ser más lentas que las ramas mal predichas, por lo que la pregunta es ¿qué tan probable es que se prediga la rama? Esa pregunta se aplica a todo tipo de ramas condicionales, ya sea generadas por ifdeclaraciones 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 sobre switchser más rápido de lo que ifes cierto. Dependería de la penalización por sucursales mal predichas, y eso realmente dependería de qué ARM.
Cody Gray
0

Una posible razón es que si los valores bajos de numson 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.

vll
fuente
1
Hmm, teoría interesante, pero para ifs vs switch tienes: xor, test, jmp vs mov, cmp jmp. Tres instrucciones, cada una con el último salto. Parece igual en el mejor de los casos, ¿no?
chacham15
3
"Una instrucción de salto que no resulta para saltar es más rápida que una que resulta para saltar". Es la predicción de la rama lo que importa.
geza