Si contra velocidad de cambio

112

Las sentencias de cambio suelen ser más rápidas que las sentencias if-else-if equivalentes (como, por ejemplo, se describe en este artículo ) debido a las optimizaciones del compilador.

¿Cómo funciona realmente esta optimización? ¿Alguien tiene una buena explicación?

Dirk Vollmar
fuente
Una posible buena respuesta: dotnetperls.com/if-switch-performance
Babak

Respuestas:

185

El compilador puede crear tablas de salto cuando corresponda. Por ejemplo, cuando usa el reflector para mirar el código producido, verá que para interruptores grandes en cadenas, el compilador generará código que usa una tabla hash para enviarlos. La tabla hash utiliza las cadenas como claves y delega los casecódigos como valores.

Esto tiene un mejor tiempo de ejecución asintótico que muchos if pruebas y en realidad es más rápido incluso para relativamente pocas cadenas.

Konrad Rudolph
fuente
6
Buena respuesta, interesante sobre la tabla hash.
BobbyShaftoe
4
También se convierten en comparaciones de árboles en algunos casos. El razonamiento es algo complejo, pero básicamente se reduce a la indirección de la tabla que neutraliza los búferes de destino de salto de CPU modernos y, por lo tanto, elimina el predictor de rama. Recuerdo vagamente un artículo en una conferencia de GCC sobre codegen para conmutadores.
olliej
Eso significa: cambiar (a) caso "x": caso "y": caso "z": // algo roto; } es más rápido que: if (a == "x" || a == "b" || a == "c") // ¿algo correcto?
yazanpro
aquí no tenemos anidado if else, solo OR entonces, ¿qué piensas?
yazanpro
@yazanpro En compiladores antiguos potencialmente sí (¡pero tenga en cuenta que la cantidad de casos es tan pequeña que podría no hacer una diferencia!). Sin embargo, los compiladores modernos hacen mucho más análisis de código. Como consecuencia, podrían darse cuenta de que estos dos fragmentos de código son equivalentes y aplicar las mismas optimizaciones. Pero esto es pura especulación de mi parte, no sé si algún compilador realmente lo hace.
Konrad Rudolph
15

Esta es una ligera simplificación, como suele ocurrir con cualquier compilador moderno que if..else if .. secuencia que una persona podría convertir trivialmente en una declaración de cambio, el compilador también lo hará. Pero solo para agregar diversión extra, el compilador no está restringido por la sintaxis, por lo que puede generar declaraciones similares a "switch" internamente que tienen una combinación de rangos, objetivos únicos, etc., y pueden (y lo hacen) hacer esto tanto para switch como para if. otras declaraciones.

De todos modos, una extensión de la respuesta de Konrad es que el compilador puede generar una tabla de salto, pero eso no está necesariamente garantizado (ni es deseable). Por una variedad de razones, las tablas de salto hacen cosas malas a los predictores de rama en los procesadores modernos, y las tablas mismas hacen cosas malas al comportamiento de la caché, por ejemplo.

switch(a) { case 0: ...; break; case 1: ...; break; }

Si un compilador generara realmente una tabla de salto para esto, probablemente sería más lento que el if..else if..código de estilo alternativo debido a que la tabla de salto anula la predicción de rama.

olliej
fuente
4

Es posible que las estadísticas de no coincidencia no sean buenas.

Si realmente descarga la fuente, se sabe que los valores de no coincidencia son 21, tanto en el caso if como en el switch. Un compilador debería poder abstraerse, sabiendo qué sentencia debería ejecutarse en todo momento, y una CPU debería poder predecir bifurcaciones correctamente.

El caso más interesante es cuando no todos los casos se rompen, en mi opinión, pero ese puede no haber sido el alcance del experimento.

Calyth
fuente
4

Las declaraciones switch / case pueden ser típicamente más rápidas en un nivel de profundidad, pero cuando comienza a entrar en 2 o más, las declaraciones switch / case comienzan a tomar 2-3 veces más que las declaraciones if / else anidadas.

Este artículo tiene algunas comparaciones de velocidad que destacan las diferencias de velocidad cuando se anidan tales declaraciones.

Por ejemplo, según sus pruebas, muestra un código como el siguiente:

if (x % 3 == 0)
            if (y % 3 == 0)
                total += 3;
            else if (y % 3 == 1)
                total += 2;
            else if (y % 3 == 2)
                total += 1;
            else
                total += 0;
        else if (x % 3 == 1)
            if (y % 3 == 0)
                total += 3;
            else if (y % 3 == 1)
                total += 2;
            else if (y % 3 == 2)
                total += 1;
            else
                total += 0;
        else if (x % 3 == 2)
            if (y % 3 == 0)
                total += 3;
            else if (y % 3 == 1)
                total += 2;
            else if (y % 3 == 2)
                total += 1;
            else
                total += 0;
        else
            if (y % 3 == 0)
                total += 3;
            else if (y % 3 == 1)
                total += 2;
            else if (y % 3 == 2)
                total += 1;
            else
                total += 0;

terminó en la mitad del tiempo que tardó en ejecutarse la instrucción switch / case equivalente:

switch (x % 3)
    {
        case 0:
            switch (y % 3)
            {
                case 0: total += 3;
                    break;
                case 1: total += 2;
                    break;
                case 2: total += 1;
                    break;
                default: total += 0;
                    break;
            }
            break;
        case 1:
            switch (y % 3)
            {
                case 0: total += 3;
                    break;
                case 1: total += 2;
                    break;
                case 2: total += 1;
                    break;
                default: total += 0;
                    break;
            }
            break;
    case 2:
            switch (y % 3)
            {
                case 0: total += 3;
                    break;
                case 1: total += 2;
                    break;
                case 2: total += 1;
                    break;
                default: total += 0;
                    break;
            }
            break;
    default:
        switch (y % 3)
        {
            case 0: total += 3;
                break;
            case 1: total += 2;
                break;
            case 2: total += 1;
                break;
            default: total += 0;
                break;
        }
        break;
    }

Sí, es un ejemplo rudimentario, pero ilustra el punto.

Entonces, una conclusión podría ser usar switch / case para tipos simples que tienen solo un nivel de profundidad, pero para comparaciones más complejas y múltiples niveles anidados, use las construcciones clásicas if / else?


fuente
-1: 1. El artículo ignoró por completo la predicción de rama, 2. los algoritmos no son exactamente iguales (el único if-else en el enlace ya está codificado más optimizado) y 3. las diferencias encontradas son tan pequeñas que nada excusa el uso de código limpio y adecuado (aproximadamente 4 ns en 10.000.000 de llamadas entre el conmutador y la misma construcción if-else)
Trojaner
Ese ejemplo no se optimizará debido a los pocos casos que tiene el bloque de interruptores. Normalmente, después de 5-6 elementos, generará una tabla de salto.
antiduh
0

La única ventaja del caso if over es cuando hay un aumento notable de la frecuencia de ocurrencia del primer caso.

No estoy seguro de dónde está exactamente el umbral, pero uso la sintaxis de casos a menos que el primer "casi siempre" pase la primera prueba.

Ralph
fuente