Qual é a diferença de desempenho relativa da instrução if / else versus switch em Java?

122

Preocupado com o desempenho do meu aplicativo da web, pergunto-me qual das declarações "if / else" ou switch é melhor em relação ao desempenho?

Anth0
fonte
6
Você tem algum motivo para pensar que o mesmo bytecode não é gerado para as duas construções?
Pascal Cuoq
2
@Pascal: Pode haver otimização feito usando a tabela look-ups, em vez de uma lista de ifetc.
jldupont
18
"Otimização prematura é a raiz de todo mal" - Donald Knuth
missingfaktor 18/01/10
104
Embora essa seja definitivamente uma otimização prematura, "A adesão irracional a uma citação mal tirada do contexto é a razão pela qual precisamos de um computador com vários núcleos de ponta apenas para exibir hoje uma GUI razoavelmente responsiva" - eu.
Lawrence Dol
2
Knuth tem uma mente precisa. Observe o qualificador "prematuro". A otimização é uma preocupação perfeitamente válida. Dito isto, um servidor está ligado à E / S e os gargalos da E / S de rede e disco são ordens de magnitude mais significativas do que qualquer outra coisa que você esteja executando no servidor.
alphazero

Respostas:

108

Isso é micro otimização e otimização prematura, que são más. Preocupe-se com a legibilidade e a manutenção do código em questão. Se houver mais de dois if/elseblocos colados ou seu tamanho for imprevisível, você poderá considerar uma switchdeclaração.

Como alternativa, você também pode pegar o polimorfismo . Primeiro, crie alguma interface:

public interface Action { 
    void execute(String input);
}

E conheça todas as implementações em algumas Map. Você pode fazer isso estaticamente ou dinamicamente:

Map<String, Action> actions = new HashMap<String, Action>();

Finalmente substitua o if/elseou switchpor algo assim (deixando verificações triviais como nullpointers de lado):

actions.get(name).execute(input);

Ele pode ser microslower do que if/elseou switch, mas o código é pelo menos muito melhor manutenção.

Enquanto você fala sobre aplicativos da web, pode usar HttpServletRequest#getPathInfo()como chave de ação (eventualmente escreva mais um código para dividir a última parte do pathinfo em um loop até que uma ação seja encontrada). Você pode encontrar aqui respostas semelhantes:

Se você está preocupado com o desempenho de aplicativos da Web Java EE em geral, também poderá achar este artigo útil. Existem outras áreas que oferecem um ganho de desempenho muito maior do que apenas a otimização (micro) do código Java bruto.

BalusC
fonte
1
ou considere o polimorfismo
jk.
Isso é realmente mais recomendado no caso de uma quantidade "imprevisível" de blocos if / else.
BalusC
73
Não sou tão rápido em descartar toda otimização inicial como "má". Ser muito agressivo é uma tolice, mas, quando confrontado com construções de legibilidade comparável, escolher uma que tenha um desempenho melhor é uma decisão apropriada.
precisa
8
A versão de pesquisa do HashMap pode ser facilmente 10 vezes mais lenta em comparação com uma instrução tableswitsch. Eu não chamaria isso de "microslower"!
X4u
7
Estou realmente interessado em conhecer o funcionamento interno do Java, no caso geral, com as declarações switch - não estou interessado em saber se alguém pensa ou não que isso está relacionado à priorização excessiva da otimização precoce. Dito isto, não tenho absolutamente nenhuma idéia de por que essa resposta é tão votada e por que é a resposta aceita ... isso não faz nada para responder à pergunta inicial.
searchengine27
125

Concordo totalmente com a opinião de que a otimização prematura é algo a ser evitado.

Mas é verdade que a Java VM possui bytecodes especiais que podem ser usados ​​para os switches ().

Consulte WM Spec ( lookupswitch e tableswitch )

Portanto, pode haver alguns ganhos de desempenho, se o código fizer parte do gráfico da CPU de desempenho.

Waverick
fonte
60
Eu me pergunto por que esse comentário não tem uma classificação mais alta: é o mais informativo de todos eles. Quero dizer: todos nós já sabemos que a otimização prematura é ruim e tal, não há necessidade de explicar isso pela milésima vez.
Folkert van Heusden
5
+1 Em stackoverflow.com/a/15621602/89818 , parece que os ganhos de desempenho estão realmente lá, e você deve ver uma vantagem se usar mais de 18 casos.
caw
52

É extremamente improvável que um if / else ou um switch sejam a fonte dos seus problemas de desempenho. Se você estiver tendo problemas de desempenho, faça uma análise de perfil de desempenho primeiro para determinar onde estão os pontos lentos. Otimização prematura é a raiz de todo o mal!

No entanto, é possível falar sobre o desempenho relativo do switch vs. if / else com as otimizações do compilador Java. Primeiro, observe que, em Java, as instruções switch operam em um domínio - número inteiro muito limitado. Em geral, você pode visualizar uma instrução switch da seguinte maneira:

switch (<condition>) {
   case c_0: ...
   case c_1: ...
   ...
   case c_n: ...
   default: ...
}

onde c_0,, c_1... e c_Nsão números inteiros que são alvos da instrução switch e <condition>devem ser resolvidos para uma expressão inteira.

  • Se esse conjunto for "denso" - ou seja, (max (c i ) + 1 - min (c i )) / n> α, onde 0 <k <α <1, onde ké maior que algum valor empírico, a A tabela de salto pode ser gerada, o que é altamente eficiente.

  • Se esse conjunto não for muito denso, mas n> = β, uma árvore de pesquisa binária poderá encontrar o destino em O (2 * log (n)), que também é eficiente também.

Para todos os outros casos, uma instrução switch é exatamente tão eficiente quanto a série equivalente de instruções if / else. Os valores precisos de α e β dependem de vários fatores e são determinados pelo módulo de otimização de código do compilador.

Finalmente, é claro, se o domínio de <condition>não for o número inteiro, uma instrução switch será completamente inútil.

John Feminella
fonte
+1. Há uma boa chance de que o tempo gasto na E / S de rede esteja ofuscando facilmente esse problema específico.
Adam Paynter
3
Note-se que os switches funcionam com mais do que apenas ints. Nos Tutoriais Java: "Uma opção funciona com os tipos de dados primitivos byte, short, char e int. Também funciona com tipos enumerados (discutidos em Tipos de enum), a classe String e algumas classes especiais que envolvem certos tipos primitivos : Caractere, byte, curto e inteiro (discutido em números e seqüências de caracteres). " Suporte para String é uma adição mais recente; adicionado no Java 7. docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html
atraudes
1
@jhonFeminella Você poderia comparar os efeitos da noção BIG O para Java7 String no Swtich em comparação com String em if / else if ..?
Kanagavelu Sugumar
Mais precisamente, o javac 8 dá uma ponderação de 3 à complexidade do tempo sobre a complexidade do espaço: stackoverflow.com/a/31032054/895245
Ciro Santilli
11

Use o interruptor!

Eu odeio manter blocos if-else! Faça um teste:

public class SpeedTestSwitch
{
    private static void do1(int loop)
    {
        int temp = 0;
        for (; loop > 0; --loop)
        {
            int r = (int) (Math.random() * 10);
            switch (r)
            {
                case 0:
                    temp = 9;
                    break;
                case 1:
                    temp = 8;
                    break;
                case 2:
                    temp = 7;
                    break;
                case 3:
                    temp = 6;
                    break;
                case 4:
                    temp = 5;
                    break;
                case 5:
                    temp = 4;
                    break;
                case 6:
                    temp = 3;
                    break;
                case 7:
                    temp = 2;
                    break;
                case 8:
                    temp = 1;
                    break;
                case 9:
                    temp = 0;
                    break;
            }
        }
        System.out.println("ignore: " + temp);
    }

    private static void do2(int loop)
    {
        int temp = 0;
        for (; loop > 0; --loop)
        {
            int r = (int) (Math.random() * 10);
            if (r == 0)
                temp = 9;
            else
                if (r == 1)
                    temp = 8;
                else
                    if (r == 2)
                        temp = 7;
                    else
                        if (r == 3)
                            temp = 6;
                        else
                            if (r == 4)
                                temp = 5;
                            else
                                if (r == 5)
                                    temp = 4;
                                else
                                    if (r == 6)
                                        temp = 3;
                                    else
                                        if (r == 7)
                                            temp = 2;
                                        else
                                            if (r == 8)
                                                temp = 1;
                                            else
                                                if (r == 9)
                                                    temp = 0;
        }
        System.out.println("ignore: " + temp);
    }

    public static void main(String[] args)
    {
        long time;
        int loop = 1 * 100 * 1000 * 1000;
        System.out.println("warming up...");
        do1(loop / 100);
        do2(loop / 100);

        System.out.println("start");

        // run 1
        System.out.println("switch:");
        time = System.currentTimeMillis();
        do1(loop);
        System.out.println(" -> time needed: " + (System.currentTimeMillis() - time));

        // run 2
        System.out.println("if/else:");
        time = System.currentTimeMillis();
        do2(loop);
        System.out.println(" -> time needed: " + (System.currentTimeMillis() - time));
    }
}

Meu código padrão C # para benchmarking

Azul amargo
fonte
Você poderia, por algum tempo, elaborar um pouco sobre como você fez isso?
DerMike 5/05
Muito obrigado pela sua atualização. Quero dizer, eles diferem por uma ordem de magnitude - o que é possível, é claro. Você tem certeza de que o compilador não apenas otimizou os switches?
DerMike 5/05
@ DerMike Não me lembro como consegui os resultados antigos. Hoje fiquei muito diferente. Mas tente você mesmo e deixe-me saber como fica.
Bitterblue 5/05
1
quando eu o executo no meu laptop; interruptor de tempo necessário: 3585, se / hora mais necessário: 3458 assim if / else é melhor :) ou não pior
halil
1
O custo dominante no teste é a geração de números aleatórios. Modifiquei o teste para gerar o número aleatório antes do loop e usei o valor temporário para retornar a r. O interruptor é quase duas vezes mais rápido que a cadeia if-else.
boneill
8

Lembro-me de ler que existem 2 tipos de instruções Switch no bytecode Java. (Eu acho que estava no 'Java Performance Tuning'. One é uma implementação muito rápida que usa os valores inteiros da instrução switch para saber o deslocamento do código a ser executado. Isso exigiria que todos os números inteiros fossem consecutivos e em um intervalo bem definido Suponho que o uso de todos os valores de um Enum também se enquadre nessa categoria.

Mas eu concordo com muitos outros pôsteres ... pode ser prematuro se preocupar com isso, a menos que seja um código muito, muito quente.

malaverdiere
fonte
4
+1 para o comentário de código quente. Se estiver no loop principal, não é prematuro.
precisa saber é o seguinte
Sim, o javac implementa switchvárias maneiras diferentes, algumas mais eficientes que outras. Em geral, a eficiência não será pior do que uma " ifescada " direta , mas há variações suficientes (especialmente com o JITC) que dificilmente será mais preciso do que isso.
Hot Licks
8

De acordo com Cliff Click em sua palestra em Java One A 2009 A Crash Course in Modern Hardware :

Hoje, o desempenho é dominado por padrões de acesso à memória. As falhas de cache dominam - a memória é o novo disco. [Slide 65]

Você pode obter os slides completos aqui .

Cliff dá um exemplo (terminando no Slide 30) mostrando que, mesmo com a CPU fazendo renomeação de registro, previsão de ramificação e execução especulativa, só é possível iniciar 7 operações em 4 ciclos de clock antes de ter que bloquear devido a duas falhas de cache que são necessárias 300 ciclos de relógio para retornar.

Então, ele diz que para acelerar seu programa, você não deve considerar esse tipo de problema menor, mas em problemas maiores, como se você está fazendo conversões desnecessárias no formato de dados, como a conversão de "SOAP → XML → DOM → SQL →… "what" passa todos os dados pelo cache ".

Jim Ferrans
fonte
4

No meu teste, o melhor desempenho é ENUM> MAP> SWITCH> IF / ELSE IF no Windows7.

import java.util.HashMap;
import java.util.Map;

public class StringsInSwitch {
public static void main(String[] args) {
    String doSomething = null;


    //METHOD_1 : SWITCH
    long start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);

        switch (input) {
        case "Hello World0":
            doSomething = "Hello World0";
            break;
        case "Hello World1":
            doSomething = "Hello World0";
            break;
        case "Hello World2":
            doSomething = "Hello World0";
            break;
        case "Hello World3":
            doSomething = "Hello World0";
            break;
        case "Hello World4":
            doSomething = "Hello World0";
            break;
        case "Hello World5":
            doSomething = "Hello World0";
            break;
        case "Hello World6":
            doSomething = "Hello World0";
            break;
        case "Hello World7":
            doSomething = "Hello World0";
            break;
        case "Hello World8":
            doSomething = "Hello World0";
            break;
        case "Hello World9":
            doSomething = "Hello World0";
            break;
        case "Hello World10":
            doSomething = "Hello World0";
            break;
        case "Hello World11":
            doSomething = "Hello World0";
            break;
        case "Hello World12":
            doSomething = "Hello World0";
            break;
        case "Hello World13":
            doSomething = "Hello World0";
            break;
        case "Hello World14":
            doSomething = "Hello World0";
            break;
        case "Hello World15":
            doSomething = "Hello World0";
            break;
        }
    }

    System.out.println("Time taken for String in Switch :"+ (System.currentTimeMillis() - start));




    //METHOD_2 : IF/ELSE IF
    start = System.currentTimeMillis();

    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);

        if(input.equals("Hello World0")){
            doSomething = "Hello World0";
        } else if(input.equals("Hello World1")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World2")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World3")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World4")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World5")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World6")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World7")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World8")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World9")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World10")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World11")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World12")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World13")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World14")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World15")){
            doSomething = "Hello World0";

        }
    }
    System.out.println("Time taken for String in if/else if :"+ (System.currentTimeMillis() - start));









    //METHOD_3 : MAP
    //Create and build Map
    Map<String, ExecutableClass> map = new HashMap<String, ExecutableClass>();
    for (int i = 0; i <= 15; i++) {
        String input = "Hello World" + (i & 0xF);
        map.put(input, new ExecutableClass(){
                            public void execute(String doSomething){
                                doSomething = "Hello World0";
                            }
                        });
    }


    //Start test map
    start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);
        map.get(input).execute(doSomething);
    }
    System.out.println("Time taken for String in Map :"+ (System.currentTimeMillis() - start));






    //METHOD_4 : ENUM (This doesn't use muliple string with space.)
    start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "HW" + (i & 0xF);
        HelloWorld.valueOf(input).execute(doSomething);
    }
    System.out.println("Time taken for String in ENUM :"+ (System.currentTimeMillis() - start));


    }

}

interface ExecutableClass
{
    public void execute(String doSomething);
}



// Enum version
enum HelloWorld {
    HW0("Hello World0"), HW1("Hello World1"), HW2("Hello World2"), HW3(
            "Hello World3"), HW4("Hello World4"), HW5("Hello World5"), HW6(
            "Hello World6"), HW7("Hello World7"), HW8("Hello World8"), HW9(
            "Hello World9"), HW10("Hello World10"), HW11("Hello World11"), HW12(
            "Hello World12"), HW13("Hello World13"), HW14("Hello World4"), HW15(
            "Hello World15");

    private String name = null;

    private HelloWorld(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void execute(String doSomething){
        doSomething = "Hello World0";
    }

    public static HelloWorld fromString(String input) {
        for (HelloWorld hw : HelloWorld.values()) {
            if (input.equals(hw.getName())) {
                return hw;
            }
        }
        return null;
    }

}





//Enum version for betterment on coding format compare to interface ExecutableClass
enum HelloWorld1 {
    HW0("Hello World0") {   
        public void execute(String doSomething){
            doSomething = "Hello World0";
        }
    }, 
    HW1("Hello World1"){    
        public void execute(String doSomething){
            doSomething = "Hello World0";
        }
    };
    private String name = null;

    private HelloWorld1(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void execute(String doSomething){
    //  super call, nothing here
    }
}


/*
 * http://stackoverflow.com/questions/338206/why-cant-i-switch-on-a-string
 * https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-3.html#jvms-3.10
 * http://forums.xkcd.com/viewtopic.php?f=11&t=33524
 */ 
Kanagavelu Sugumar
fonte
Time taken for String in Switch :3235 Time taken for String in if/else if :3143 Time taken for String in Map :4194 Time taken for String in ENUM :2866
Halil
@halil Eu não tenho certeza de como esse código funciona em ambientes diferentes, mas você mencionou se / elseif é melhor que Switch e Map, que eu não sou capaz de convencer, pois se / elseif precisa executar mais vezes que não é igual a comparação.
Kanagavelu Sugumar
2

Para a maioria switche a maioria dos if-then-elseblocos, não consigo imaginar que haja alguma preocupação significativa ou significativa relacionada ao desempenho.

Mas eis o seguinte: se você estiver usando um switchbloco, seu próprio uso sugere que você esteja ativando um valor obtido de um conjunto de constantes conhecidas em tempo de compilação. Nesse caso, você realmente não deveria usar switchinstruções, se puder usar enummétodos com constantes específicas.

Comparado a uma switchdeclaração, um enum fornece melhor segurança e código de tipo, mais fáceis de manter. As enums podem ser projetadas para que, se uma constante for adicionada ao conjunto de constantes, seu código não seja compilado sem fornecer um método específico para a constante para o novo valor. Por outro lado, esquecer de adicionar um novo casea um switchbloco às vezes só pode ser detectado em tempo de execução se você tiver a sorte de configurar seu bloco para lançar uma exceção.

O desempenho entre switche um enummétodo específico de constante não deve ser significativamente diferente, mas o último é mais legível, mais seguro e mais fácil de manter.

scottb
fonte