Quelle est la différence de performance relative entre l'instruction if / else et l'instruction switch en Java?

123

En me souciant des performances de mon application Web, je me demande laquelle des instructions «if / else» ou switch est la meilleure en termes de performances?

Anth0
la source
6
Avez-vous des raisons de penser que le même bytecode n'est pas généré pour les deux constructions?
Pascal Cuoq
2
@Pascal: une optimisation pourrait être effectuée en utilisant des recherches de table au lieu d'une liste de ifetc.
jldupont
18
"L'optimisation prématurée est la racine de tout mal" - Donald Knuth
missingfaktor
104
Bien que ce soit définitivement une optimisation prématurée, "L'adhésion insensée à une citation prise hors de son contexte est la raison pour laquelle nous avons besoin d'un ordinateur multi-cœur haut de gamme juste pour afficher une interface graphique raisonnablement réactive aujourd'hui" - Moi.
Lawrence Dol
2
Knuth a un esprit précis. Veuillez noter le qualificatif «prématuré». L'optimisation est une préoccupation parfaitement valable. Cela dit, un serveur est lié aux E / S et les goulots d'étranglement des E / S réseau et disque sont des ordres de grandeur plus importants que tout ce que vous avez sur votre serveur.
alphazero

Réponses:

108

C'est la micro-optimisation et l'optimisation prématurée, qui sont mauvaises. Se soucier plutôt de la lisibilité et de la maintenabilité du code en question. S'il y a plus de deux if/elseblocs collés ensemble ou si sa taille est imprévisible, alors vous pouvez fortement envisager une switchdéclaration.

Alternativement, vous pouvez également récupérer Polymorphism . Commencez par créer une interface:

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

Et obtenez toutes les implémentations dans certains Map. Vous pouvez le faire de manière statique ou dynamique:

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

Enfin, remplacez le if/elseou switchpar quelque chose comme ceci (en laissant de côté les vérifications triviales comme les pointeurs nuls):

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

Il est peut- être plus lent que if/elseou switch, mais le code est au moins beaucoup mieux maintenable.

Lorsque vous parlez d'applications Web, vous pouvez utiliser HttpServletRequest#getPathInfo()comme clé d'action (éventuellement écrire un peu plus de code pour séparer la dernière partie de pathinfo en une boucle jusqu'à ce qu'une action soit trouvée). Vous pouvez trouver ici des réponses similaires:

Si vous vous inquiétez des performances de l'application Web Java EE en général, cet article peut également vous être utile. Il existe d'autres domaines qui donnent un gain de performances beaucoup plus important que la seule (micro) optimisation du code Java brut.

BalusC
la source
1
ou considérez plutôt le polymorphisme
jk.
C'est en effet plus recommandé en cas de quantité "imprévisible" de blocs if / else.
BalusC
73
Je ne suis pas si prompt à rejeter toutes les premières optimisations comme «mauvaises». Être trop agressif est insensé, mais face à des constructions de lisibilité comparable, choisir celle dont les performances sont meilleures est une décision appropriée.
Brian Knoblauch
8
La version de recherche HashMap peut facilement être 10 fois plus lente par rapport à une instruction tablewitsch. Je n'appellerais pas cela "microslower"!
x4u
7
Je suis intéressé à connaître le fonctionnement interne de Java dans le cas général des instructions de commutation - je ne suis pas intéressé de savoir si quelqu'un pense ou non que cela est lié à une sur-priorisation de l'optimisation précoce. Cela étant dit, je n'ai absolument aucune idée pourquoi cette réponse est tellement votée et pourquoi c'est la réponse acceptée ... cela ne fait rien pour répondre à la question initiale.
searchengine27
125

Je suis totalement d'accord avec l'opinion selon laquelle l'optimisation prématurée est quelque chose à éviter.

Mais il est vrai que la machine virtuelle Java a des bytecodes spéciaux qui pourraient être utilisés pour les switch ().

Voir WM Spec ( lookupswitch et tableswitch )

Il pourrait donc y avoir des gains de performances, si le code fait partie du graphique des performances du processeur.

Waverick
la source
60
Je me demande pourquoi ce commentaire n'est pas mieux noté: c'est le plus informatif de tous. Je veux dire: nous savons tous déjà que l'optimisation prématurée est mauvaise et ainsi de suite, inutile de l'expliquer pour la 1000e fois.
Folkert van Heusden
5
+1 Depuis stackoverflow.com/a/15621602/89818, il semble que les gains de performances sont vraiment là, et vous devriez voir un avantage si vous utilisez plus de 18 cas.
caw
52

Il est extrêmement improbable qu'un if / else ou un commutateur soit la source de vos problèmes de performances. Si vous rencontrez des problèmes de performances, vous devez d'abord effectuer une analyse de profilage des performances pour déterminer où se trouvent les points lents. L'optimisation prématurée est la racine de tout Mal!

Néanmoins, il est possible de parler des performances relatives du commutateur par rapport à if / else avec les optimisations du compilateur Java. Notez d'abord qu'en Java, les instructions switch fonctionnent sur un domaine très limité - les entiers. En général, vous pouvez afficher une instruction switch comme suit:

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

c_0,, c_1... et c_Nsont des nombres entiers qui sont les cibles de l'instruction switch et <condition>doivent se résoudre en une expression entière.

  • Si cet ensemble est "dense" - c'est-à-dire (max (c i ) + 1 - min (c i )) / n> α, où 0 <k <α <1, où kest plus grand qu'une certaine valeur empirique, a une table de saut peut être générée, ce qui est très efficace.

  • Si cet ensemble n'est pas très dense, mais n> = β, un arbre de recherche binaire peut trouver la cible dans O (2 * log (n)) qui est toujours aussi efficace.

Dans tous les autres cas, une instruction switch est exactement aussi efficace que la série équivalente d'instructions if / else. Les valeurs précises de α et β dépendent d'un certain nombre de facteurs et sont déterminées par le module d'optimisation de code du compilateur.

Enfin, bien sûr, si le domaine de <condition>n'est pas les nombres entiers, une instruction switch est complètement inutile.

John Feminella
la source
+1. Il y a de fortes chances que le temps passé sur les E / S réseau éclipse facilement ce problème particulier.
Adam Paynter
3
Il convient de noter que les commutateurs fonctionnent avec plus que de simples ints. Extrait des didacticiels Java: «Un commutateur fonctionne avec les types de données primitifs byte, short, char et int. Il fonctionne également avec les types énumérés (décrits dans Enum Types), la classe String et quelques classes spéciales qui encapsulent certains types primitifs : Caractère, octet, court et entier (décrit dans Nombres et chaînes). " La prise en charge de String est un ajout plus récent; ajouté dans Java 7. docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html
atraudes
1
@jhonFeminella Pourriez-vous s'il vous plaît comparer les effets de la notion BIG O pour Java7 String dans Swtich par rapport à String dans if / else if ..?
Kanagavelu Sugumar
Plus précisément, javac 8 donne un poids de 3 à la complexité du temps sur la complexité de l'espace: stackoverflow.com/a/31032054/895245
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
11

Utilisez le commutateur!

Je déteste maintenir des blocs if-else! Faites un test:

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

Mon code standard C # pour l'analyse comparative

Bleu amer
la source
Pourriez-vous s'il vous plaît (parfois) expliquer un peu comment vous avez évalué cela?
DerMike
Merci beaucoup pour votre mise à jour. Je veux dire, ils diffèrent d'un ordre de grandeur - ce qui est bien sûr possible. Etes-vous sûr que le compilateur n'a pas simplement optimisé les switches loin?
DerMike
@DerMike Je ne me souviens pas comment j'ai obtenu les anciens résultats. Je suis devenu très différent aujourd'hui. Mais essayez-le vous-même et dites-moi comment cela se passe.
Bitterblue
1
quand je l'exécute sur mon ordinateur portable; temps de commutation nécessaire: 3585, si / sinon temps nécessaire: 3458 donc si / sinon est meilleur :) ou pas pire
halil
1
Le coût dominant du test est la génération de nombres aléatoires. J'ai modifié le test pour générer le nombre aléatoire avant la boucle et utilisé la valeur de température pour revenir dans r. L'interrupteur est alors presque deux fois plus rapide que la chaîne if-else.
boneill
8

Je me souviens avoir lu qu'il existe 2 types d'instructions Switch dans le bytecode Java. (Je pense que c'était dans 'Java Performance Tuning' One est une implémentation très rapide qui utilise les valeurs entières de l'instruction switch pour connaître le décalage du code à exécuter. Cela nécessiterait que tous les entiers soient consécutifs et dans une plage bien définie Je suppose que l'utilisation de toutes les valeurs d'un Enum tomberait également dans cette catégorie.

Je suis cependant d'accord avec beaucoup d'autres affiches ... il est peut-être prématuré de s'inquiéter à ce sujet, à moins que ce ne soit du code très très chaud.

malaverdiere
la source
4
+1 pour le commentaire hot code. Si c'est dans votre boucle principale, ce n'est pas prématuré.
KingAndrew le
Oui, javac implémente switchplusieurs manières différentes, certaines plus efficaces que d'autres. En général, l'efficacité ne sera pas pire qu'une simple « iféchelle», mais il y a suffisamment de variations (surtout avec le JITC) qu'il est difficile d'être beaucoup plus précis que cela.
Hot Licks
8

Selon Cliff Click dans son exposé sur Java One en 2009, un cours intensif sur le matériel moderne :

Aujourd'hui, les performances sont dominées par les modèles d'accès à la mémoire. Les erreurs de cache dominent - la mémoire est le nouveau disque. [Diapositive 65]

Vous pouvez obtenir ses diapositives complètes ici .

Cliff donne un exemple (finissant sur la diapositive 30) montrant que même avec le processeur effectuant un renommage de registre, une prédiction de branche et une exécution spéculative, il ne peut démarrer que 7 opérations en 4 cycles d'horloge avant d'avoir à bloquer en raison de deux échecs de cache qui prennent 300 cycles d'horloge pour revenir.

Donc, il dit que pour accélérer votre programme, vous ne devriez pas vous pencher sur ce genre de problème mineur, mais sur des problèmes plus importants tels que si vous effectuez des conversions de format de données inutiles, telles que la conversion de "SOAP → XML → DOM → SQL → ... "qui" passe toutes les données à travers le cache ".

Jim Ferrans
la source
4

Dans mon test, la meilleure performance est ENUM> MAP> SWITCH> IF / ELSE IF dans 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
la source
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 Je ne sais pas comment ce code fonctionne sur différents environnements, mais vous avez mentionné si / elseif est meilleur que Switch et Map, que je ne suis pas en mesure de convaincre car si / elseif doit effectuer plus de fois aucune comparaison.
Kanagavelu Sugumar
2

Pour la plupart switchet la plupart des if-then-elseblocs, je ne peux pas imaginer qu'il existe des problèmes de performances appréciables ou significatifs.

Mais voici le problème: si vous utilisez un switchbloc, son utilisation même suggère que vous basculez sur une valeur prise à partir d'un ensemble de constantes connues au moment de la compilation. Dans ce cas, vous ne devriez vraiment pas utiliser d' switchinstructions du tout si vous pouvez utiliser unenum des méthodes spécifiques aux constantes.

Par rapport à une switchinstruction, une énumération fournit une meilleure sécurité de type et un code plus facile à gérer. Les énumérations peuvent être conçues de sorte que si une constante est ajoutée à l'ensemble de constantes, votre code ne se compilera pas sans fournir une méthode spécifique à la constante pour la nouvelle valeur. D'un autre côté, oublier d'ajouter un nouveau caseà un switchbloc ne peut parfois être intercepté au moment de l'exécution que si vous avez la chance d'avoir configuré votre bloc pour lever une exception.

Les performances entre switchet une enumméthode spécifique à une constante ne doivent pas être significativement différentes, mais cette dernière est plus lisible, plus sûre et plus facile à entretenir.

Scottb
la source