Por que mudar é mais rápido do que se

116

Muitos livros de Java descrevem a switchinstrução como sendo mais rápida do que a if elseinstrução. Mas eu não descobri em lugar nenhum porque o switch é mais rápido do que se .

Exemplo

Eu tenho uma situação em que tenho que escolher qualquer um dos dois itens. Eu posso usar qualquer um

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

ou

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

considerando item e BREAD é um valor int constante.

No exemplo acima, qual ação é mais rápida e por quê?

Jacob van Lingen
fonte
Talvez esta seja uma resposta também para java: stackoverflow.com/questions/767821/…
Tobias
19
Em geral, da Wikipedia : Se o intervalo de valores de entrada é identificável 'pequeno' e tem apenas algumas lacunas, alguns compiladores que incorporam um otimizador podem realmente implementar a instrução switch como uma tabela de ramificação ou uma matriz de ponteiros de função indexados em vez de um longa série de instruções condicionais. Isso permite que a instrução switch determine instantaneamente qual ramificação executar sem precisar passar por uma lista de comparações.
Felix Kling
A principal resposta a esta pergunta explica muito bem. Este artigo também explica tudo muito bem.
bezmax
Espero que, na maioria das circunstâncias, um compilador de otimização seja capaz de gerar código com características de desempenho semelhantes. Em qualquer caso, você teria que ligar muitos milhões de vezes para notar qualquer diferença.
Mitch Wheat
2
Você deve ter cuidado com livros que fazem afirmações como esta sem explicação / prova / raciocínio.
matt b

Respostas:

110

Porque existem bytecodes especiais que permitem uma avaliação eficiente da instrução switch quando há muitos casos.

Se implementado com instruções IF, você teria uma verificação, um salto para a próxima cláusula, uma verificação, um salto para a próxima cláusula e assim por diante. Com switch, a JVM carrega o valor a ser comparado e itera por meio da tabela de valores para encontrar uma correspondência, o que é mais rápido na maioria dos casos.

Daniel
fonte
6
A iteração não traduz para "verificar, pular"?
quinze
17
@fivetwentysix: Não, consulte isso para obter informações: artima.com/underthehood/flowP.html . Citação do artigo: Quando a JVM encontra uma instrução de switch de tabela, ela pode simplesmente verificar se a chave está dentro do intervalo definido por baixo e alto. Caso contrário, ele usa o deslocamento de ramificação padrão. Nesse caso, ele apenas subtrai baixo da chave para obter um deslocamento na lista de deslocamentos de ramificação. Dessa maneira, ele pode determinar o deslocamento de ramificação apropriado sem ter que verificar cada valor de caso.
bezmax de
1
(i) a switchnão pode ser traduzido em uma tableswitchinstrução bytecode - pode se tornar uma lookupswitchinstrução que executa de forma semelhante a um if / else (ii) até mesmo tableswitchinstruções de bytecode podem ser compiladas em uma série de if / else pelo JIT, dependendo dos fatores como o número de cases.
assylias
1
tableswitchvs loopuswitch: stackoverflow.com/questions/10287700/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
34

Uma switchdeclaração nem sempre é mais rápida do que uma ifdeclaração. Escala melhor do que uma longa lista de if-elseinstruções, pois switchpode realizar uma pesquisa com base em todos os valores. No entanto, para uma condição curta, não será mais rápido e pode ser mais lento.

Peter Lawrey
fonte
5
Limite "longo". Maior que 5? Maior que 10? ou mais como 20 - 30?
vanderwyst de
11
Eu suspeito que depende. Para mim, suas 3 ou mais sugestões switchseriam mais claras, senão mais rápidas.
Peter Lawrey
Sob quais condições poderia ser mais lento?
Eric
1
@Eric é mais lento para um pequeno número de valores esp String ou int que são esparsos.
Peter Lawrey
8

A JVM atual tem dois tipos de códigos de byte de switch: LookupSwitch e TableSwitch.

Cada caso em uma instrução switch tem um deslocamento inteiro, se esses deslocamentos são contíguos (ou principalmente contíguos sem grandes lacunas) (caso 0: caso 1: caso 2, etc.), então TableSwitch é usado.

Se os deslocamentos estiverem espalhados com grandes lacunas (caso 0: caso 400: caso 93748 :, etc.), então LookupSwitch é usado.

A diferença, em resumo, é que TableSwitch é feito em tempo constante porque cada valor dentro da faixa de valores possíveis recebe um deslocamento de código de byte específico. Portanto, quando você atribui à instrução um deslocamento de 3, ela sabe pular 3 para encontrar o branch correto.

O switch de pesquisa usa uma pesquisa binária para encontrar a ramificação de código correta. Isso é executado em tempo O (log n), que ainda é bom, mas não é o melhor.

Para obter mais informações sobre isso, consulte aqui: Diferença entre LookupSwitch e TableSwitch da JVM?

Portanto, quanto a qual é o mais rápido, use esta abordagem: Se você tiver 3 ou mais casos cujos valores são consecutivos ou quase consecutivos, sempre use um switch.

Se você tiver 2 casos, use uma instrução if.

Para qualquer outra situação, a troca é provavelmente mais rápida, mas não é garantida, uma vez que a pesquisa binária em LookupSwitch pode atingir um cenário ruim.

Além disso, lembre-se de que a JVM executará otimizações JIT nas instruções if que tentarão colocar primeiro o branch mais ativo no código. Isso é chamado de "Previsão de Branch". Para obter mais informações sobre isso, consulte aqui: https://dzone.com/articles/branch-prediction-in-java

Suas experiências podem variar. Não sei se a JVM não executa uma otimização semelhante no LookupSwitch, mas aprendi a confiar nas otimizações JIT e a não tentar ser mais esperto que o compilador.

HesNotTheStig
fonte
1
Depois de postar isso, percebi que "trocar expressões" e "correspondência de padrões" estão chegando ao Java, possivelmente assim que o Java 12. openjdk.java.net/jeps/325 openjdk.java.net/jeps/305 Nada é concreto ainda, mas parece que isso tornará switchum recurso de linguagem ainda mais poderoso. A correspondência de padrões, por exemplo, permitirá instanceofpesquisas muito mais suaves e de desempenho . No entanto, acho que é seguro presumir que, para cenários switch / if básicos, a regra que mencionei ainda se aplicará.
HesNotTheStig
1

Portanto, se você planeja ter muitos pacotes, a memória não é realmente um grande custo atualmente e os arrays são muito rápidos. Você também não pode confiar em uma instrução switch para gerar automaticamente uma tabela de salto e, como tal, é mais fácil gerar você mesmo o cenário da tabela de salto. Como você pode ver no exemplo abaixo, assumimos um máximo de 255 pacotes.

Para obter o resultado abaixo, você precisa de abstração ... Não vou explicar como isso funciona, então espero que você tenha uma compreensão disso.

Eu atualizei isso para definir o tamanho do pacote para 255 se você precisar de mais do que você terá que fazer uma verificação de limites para (id <0) || (id> comprimento).

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);
}

Edite, já que eu uso muito uma tabela de salto em C ++, agora vou mostrar um exemplo de uma tabela de salto de ponteiro de função. Este é um exemplo muito genérico, mas eu o executei e ele funciona corretamente. Lembre-se de que você deve definir o ponteiro como NULL, C ++ não fará isso automaticamente como em 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;
}

Outro ponto que gostaria de mencionar é o famoso Divide and Conquer. Portanto, minha ideia de array acima de 255 poderia ser reduzida a não mais de 8 declarações if como o pior cenário.

Ou seja, mas tenha em mente que fica confuso e difícil de gerenciar rapidamente e minha outra abordagem é geralmente melhor, mas isso é utilizado em casos em que os arrays simplesmente não funcionam. Você precisa descobrir seu caso de uso e quando cada situação funciona melhor. Assim como você não gostaria de usar nenhuma dessas abordagens se tivesse apenas algumas verificações.

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
fonte
2
Por que a dupla indireção? Visto que o ID deve ser restrito de qualquer maneira, por que não apenas verificar os limites do ID de entrada 0 <= id < packets.lengthe garantir packets[id]!=nulle depois fazer packets[id].execute(data)?
Lawrence Dol
sim, desculpe pela resposta tardia olhei para isso de novo .. e eu não tenho ideia do que diabos eu estava pensando, atualizei o post lol e limitei os pacotes ao tamanho de um byte não assinado, então nenhuma verificação de comprimento é necessária.
Jeremy Trifilo
0

No nível do bytecode, a variável de assunto é carregada apenas uma vez no registro do processador a partir de um endereço de memória no arquivo .class estruturado carregado pelo Runtime, e isso está em uma instrução switch; enquanto em uma instrução if, uma instrução jvm diferente é produzida por seu DE compilador de código, e isso requer que cada variável seja carregada em registradores, embora a mesma variável seja usada como na instrução if anterior seguinte. Se você conhece a codificação em linguagem assembly, isso seria lugar-comum; embora os coxes compilados em java não sejam bytecode, ou código de máquina direto, o conceito condicional deste ainda é consistente. Bem, tentei evitar detalhes técnicos mais profundos ao explicar. Espero ter deixado o conceito claro e desmistificado. Obrigado.

Verso Villalon Gamboa
fonte