Por qué cambiar es más rápido que si

116

Muchos libros de Java describen la switchdeclaración como más rápida que la if elsedeclaración. Pero no descubrí en ninguna parte por qué el cambio es más rápido que si .

Ejemplo

Tengo una situación en la que tengo que elegir un elemento de dos. Puedo usar cualquiera de los dos

switch (item) {
    case BREAD:
        //eat Bread
        break;
    default:
        //leave the restaurant
}

o

if (item == BREAD) {
    //eat Bread
} else {
    //leave the restaurant
}

considerando item y BREAD es un valor int constante.

En el ejemplo anterior, ¿cuál es la acción más rápida y por qué?

Jacob van Lingen
fuente
Tal vez esta sea una respuesta también para java: stackoverflow.com/questions/767821/…
Tobias
19
En general, de Wikipedia : si el rango de valores de entrada es identificable como 'pequeño' y tiene solo unos pocos huecos, algunos compiladores que incorporan un optimizador pueden realmente implementar la declaración de cambio como una tabla de rama o una matriz de punteros de función indexados en lugar de un larga serie de instrucciones condicionales. Esto permite que la declaración de cambio determine instantáneamente qué rama ejecutar sin tener que pasar por una lista de comparaciones.
Felix Kling
La respuesta principal a esta pregunta lo explica bastante bien. Este artículo también explica todo bastante bien.
bezmax
Espero que, en la mayoría de las circunstancias, un compilador optimizador pueda generar código que tenga características de rendimiento similares. En cualquier caso, tendrías que llamar muchos millones de veces para notar alguna diferencia.
Mitch Wheat
2
Debe tener cuidado con los libros que hacen declaraciones como esta sin explicación / prueba / razonamiento.
Matt b

Respuestas:

110

Porque hay códigos de bytes especiales que permiten una evaluación eficiente de las declaraciones de conmutación cuando hay muchos casos.

Si se implementa con declaraciones IF, tendría un control, un salto a la siguiente cláusula, un control, un salto a la siguiente cláusula, etc. Con el interruptor, la JVM carga el valor para comparar y recorre la tabla de valores para encontrar una coincidencia, lo que es más rápido en la mayoría de los casos.

Daniel
fuente
6
¿No se traduce iterar como "comprobar, saltar"?
fivetwentysix
17
@fivetwentysix: No, consulte esto para obtener información: artima.com/underthehood/flowP.html . Cita del artículo: Cuando la JVM encuentra una instrucción de conmutador de tabla, simplemente puede verificar si la clave está dentro del rango definido por bajo y alto. De lo contrario, toma el desplazamiento de rama predeterminado. Si es así, simplemente resta baja de clave para obtener un desplazamiento en la lista de desplazamientos de rama. De esta manera, puede determinar el desplazamiento de rama apropiado sin tener que verificar cada valor de caso.
bezmax
1
(i) una switchpuede no traducirse en una tableswitchinstrucción de código de bytes; puede convertirse en una lookupswitchinstrucción que se desempeñe de manera similar a un if / else (ii) incluso las tableswitchinstrucciones de un código de bytes pueden ser compiladas en una serie de if / else por el JIT, dependiendo de los factores como el número de cases.
Assylias
1
tableswitchvs loopuswitch: stackoverflow.com/questions/10287700/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
34

Una switchdeclaración no siempre es más rápida que una ifdeclaración. Se escala mejor que una larga lista de if-elsedeclaraciones, ya que switchpuede realizar una búsqueda basada en todos los valores. Sin embargo, para una condición breve no será más rápido y podría ser más lento.

Peter Lawrey
fuente
5
Restrinja "largo". ¿Mayor que 5? ¿Mayor que 10? o más como 20 - 30?
vanderwyst
11
Sospecho que depende. Para mí, sus 3 o más sugerencias switchserían más claras, si no más rápidas.
Peter Lawrey
¿En qué condiciones podría ser más lento?
Eric
1
@Eric es más lento para una pequeña cantidad de valores esp String o int que son escasos.
Peter Lawrey
8

La JVM actual tiene dos tipos de códigos de bytes de conmutación: LookupSwitch y TableSwitch.

Cada caso en una instrucción switch tiene un desplazamiento entero, si estos desplazamientos son contiguos (o en su mayoría contiguos sin espacios grandes) (caso 0: caso 1: caso 2, etc.), entonces se usa TableSwitch.

Si las compensaciones se extienden con grandes espacios (caso 0: caso 400: caso 93748 :, etc.), se utiliza LookupSwitch.

La diferencia, en resumen, es que TableSwitch se realiza en tiempo constante porque a cada valor dentro del rango de valores posibles se le asigna un desplazamiento de código de byte específico. Por lo tanto, cuando le da a la declaración un desplazamiento de 3, sabe que debe saltar 3 para encontrar la rama correcta.

El interruptor de búsqueda utiliza una búsqueda binaria para encontrar la rama de código correcta. Esto se ejecuta en tiempo O (log n), que sigue siendo bueno, pero no el mejor.

Para obtener más información sobre esto, consulte aquí: ¿ Diferencia entre LookupSwitch y TableSwitch de JVM?

En cuanto a cuál es el más rápido, utilice este enfoque: si tiene 3 o más casos cuyos valores son consecutivos o casi consecutivos, utilice siempre un interruptor.

Si tiene 2 casos, use una declaración if.

Para cualquier otra situación, lo más probable es que el cambio sea más rápido, pero no está garantizado, ya que la búsqueda binaria en LookupSwitch podría tener un mal escenario.

Además, tenga en cuenta que la JVM ejecutará optimizaciones JIT en declaraciones if que intentarán colocar la rama más activa primero en el código. Esto se llama "Predicción de ramas". Para obtener más información sobre esto, consulte aquí: https://dzone.com/articles/branch-prediction-in-java

Tus experiencias pueden variar. No sé si la JVM no ejecuta una optimización similar en LookupSwitch, pero he aprendido a confiar en las optimizaciones de JIT y a no intentar burlarme del compilador.

HesNotTheStig
fuente
1
Desde que publiqué esto, me ha llamado la atención que las "expresiones de cambio" y la "coincidencia de patrones" llegarán a Java, posiblemente tan pronto como Java 12. openjdk.java.net/jeps/325 openjdk.java.net/jeps/305 Aún no hay nada concreto, pero parece que harán switchuna función de lenguaje aún más poderosa. La coincidencia de patrones, por ejemplo, permitirá instanceofbúsquedas mucho más fluidas y eficaces. Sin embargo, creo que es seguro asumir que para escenarios básicos de switch / if, la regla que mencioné aún se aplicará.
HesNotTheStig
1

Entonces, si planea tener muchos paquetes, la memoria no es realmente un gran costo en estos días y los arreglos son bastante rápidos. Tampoco puede confiar en una declaración de cambio para generar automáticamente una tabla de salto y, como tal, es más fácil generar el escenario de la tabla de salto usted mismo. Como puede ver en el siguiente ejemplo, asumimos un máximo de 255 paquetes.

Para obtener el resultado a continuación, necesita abstracción ... no voy a explicar cómo funciona, así que espero que lo entienda.

Actualicé esto para establecer el tamaño del paquete en 255 si necesita más, entonces tendrá que hacer una verificación de límites para (id <0) || (id> longitud).

Packets[] packets = new Packets[255];

static {
     packets[0] = new Login(6);
     packets[2] = new Logout(8);
     packets[4] = new GetMessage(1);
     packets[8] = new AddFriend(0);
     packets[11] = new JoinGroupChat(7); // etc... not going to finish.
}

public void handlePacket(IncomingData data)
{
    int id = data.readByte() & 0xFF; //Secure value to 0-255.

    if (packet[id] == null)
        return; //Leave if packet is unhandled.

    packets[id].execute(data);
}

Editar ya que uso mucho una tabla de salto en C ++ ahora mostraré un ejemplo de una tabla de salto de puntero de función. Este es un ejemplo muy genérico, pero lo ejecuté y funciona correctamente. Tenga en cuenta que debe establecer el puntero en NULL, C ++ no hará esto automáticamente como en Java.

#include <iostream>

struct Packet
{
    void(*execute)() = NULL;
};

Packet incoming_packet[255];
uint8_t test_value = 0;

void A() 
{ 
    std::cout << "I'm the 1st test.\n";
}

void B() 
{ 
    std::cout << "I'm the 2nd test.\n";
}

void Empty() 
{ 

}

void Update()
{
    if (incoming_packet[test_value].execute == NULL)
        return;

    incoming_packet[test_value].execute();
}

void InitializePackets()
{
    incoming_packet[0].execute = A;
    incoming_packet[2].execute = B;
    incoming_packet[6].execute = A;
    incoming_packet[9].execute = Empty;
}

int main()
{
    InitializePackets();

    for (int i = 0; i < 512; ++i)
    {
        Update();
        ++test_value;
    }
    system("pause");
    return 0;
}

También otro punto que me gustaría mencionar es el famoso Divide and Conquer. Por lo tanto, mi idea de matriz de 255 anterior podría reducirse a no más de 8 declaraciones if como el peor de los casos.

Es decir, pero tenga en cuenta que se vuelve complicado y difícil de administrar rápidamente y mi otro enfoque es generalmente mejor, pero esto se utiliza en los casos en que las matrices simplemente no lo cortan. Debe averiguar su caso de uso y cuándo funciona mejor cada situación. Del mismo modo que no querría utilizar ninguno de estos enfoques si solo tiene unos pocos controles.

If (Value >= 128)
{
   if (Value >= 192)
   {
        if (Value >= 224)
        {
             if (Value >= 240)
             {
                  if (Value >= 248)
                  {
                      if (Value >= 252)
                      {
                          if (Value >= 254)
                          {
                              if (value == 255)
                              {

                              } else {

                              }
                          }
                      }
                  }
             }      
        }
   }
}
Jeremy Trifilo
fuente
2
¿Por qué la doble indirecta? Dado que la identificación debe estar restringida de todos modos, ¿por qué no simplemente verificar los límites de la identificación entrante como 0 <= id < packets.lengthy asegurarse packets[id]!=nully luego hacerlo packets[id].execute(data)?
Lawrence Dol
Sí, lo siento por la respuesta tardía, miré esto de nuevo ... y no tengo idea de qué demonios estaba pensando. Actualicé la publicación jajaja y limité los paquetes al tamaño de un byte sin firmar, por lo que no se necesitan verificaciones de longitud.
Jeremy Trifilo
0

En el nivel de código de bytes, la variable sujeto se carga solo una vez en el registro del procesador desde una dirección de memoria en el archivo estructurado .class cargado por Runtime, y esto es en una declaración de cambio; mientras que en una instrucción if, su DE de compilación de código produce una instrucción jvm diferente, y esto requiere que cada variable se cargue en los registros aunque se use la misma variable que en la siguiente instrucción if precedente. Si conoce la codificación en lenguaje ensamblador, esto sería un lugar común; aunque los cox compilados en java no son bytecode, o código de máquina directo, el concepto condicional del mismo sigue siendo consistente. Bueno, traté de evitar tecnicismos más profundos al explicar. Espero haber dejado el concepto claro y desmitificado. Gracias.

Verso Villalon Gamboa
fuente