Si contre la vitesse de commutation

112

Les instructions Switch sont généralement plus rapides que les instructions if-else-if équivalentes (comme par exemple décrites dans cet article ) en raison des optimisations du compilateur.

Comment cette optimisation fonctionne-t-elle réellement? Quelqu'un at-il une bonne explication?

Dirk Vollmar
la source
Une bonne réponse possible: dotnetperls.com/if-switch-performance
Babak

Réponses:

185

Le compilateur peut créer des tables de saut le cas échéant. Par exemple, lorsque vous utilisez le réflecteur pour regarder le code produit, vous verrez que pour les gros commutateurs sur les chaînes, le compilateur générera en fait du code qui utilise une table de hachage pour les distribuer. La table de hachage utilise les chaînes comme clés et les délègue aux casecodes comme valeurs.

Cela a un meilleur temps d'exécution asymptotique que beaucoup de iftests chaînés et est en fait plus rapide même pour relativement peu de chaînes.

Konrad Rudolph
la source
6
Bonne réponse, intéressante à propos de la table de hachage.
BobbyShaftoe le
4
Ils se convertissent également en comparaisons d'arbres dans certains cas. Le raisonnement est quelque peu complexe, mais se résume essentiellement à la stérilisation par indirection de la table des tampons cibles de saut de processeur modernes et efface ainsi le prédicteur de branche. Je me souviens vaguement d'un article lors d'une conférence du GCC sur le codegen pour les commutateurs.
olliej
Cela signifie: switch (a) case "x": case "y": case "z": // quelque chose de break; } est plus rapide que: if (a == "x" || a == "b" || a == "c") // quelque chose ne va pas?
yazanpro
ici, nous n'avons pas imbriqué sinon, seulement OU alors qu'en pensez-vous?
yazanpro
@yazanpro Sur les anciens compilateurs potentiellement oui (mais notez que le nombre de cas est si petit que cela ne fera peut-être pas de différence!). Les compilateurs modernes font cependant beaucoup plus d'analyse de code. En conséquence, ils pourraient comprendre que ces deux extraits de code sont équivalents et appliquer les mêmes optimisations. Mais ce n'est que pure spéculation de ma part, je ne sais pas si un compilateur le fait réellement.
Konrad Rudolph
15

Il s'agit d'une légère simplification, car généralement tout compilateur moderne qui rencontre une if..else if ..séquence qui pourrait être convertie de manière triviale en une instruction switch par une personne, le compilateur le fera également. Mais juste pour ajouter plus de plaisir, le compilateur n'est pas limité par la syntaxe et peut donc générer en interne des instructions de type "switch" qui ont un mélange de plages, de cibles uniques, etc. - et ils peuvent (et font) le faire à la fois pour switch et if. instructions .else.

Quoi qu'il en soit, une extension de la réponse de Konrad est que le compilateur peut générer une table de saut, mais ce n'est pas nécessairement garanti (ni souhaitable). Pour diverses raisons, les tables de saut font de mauvaises choses aux prédicteurs de branche sur les processeurs modernes, et les tables elles-mêmes font de mauvaises choses pour mettre en cache le comportement, par exemple.

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

Si un compilateur générait réellement une table de saut pour cela, il serait probablement plus lent que le if..else if..code de style alternatif à cause de la table de saut qui annule la prédiction de branche.

olliej
la source
4

Les statistiques sans match peuvent ne pas être bonnes.

Si vous téléchargez réellement la source, les valeurs sans correspondance sont connues pour être 21, à la fois dans le cas if et switch. Un compilateur doit être capable d'abstraction, sachant quelle instruction doit être exécutée à tout moment, et un processeur doit être capable de prédire correctement les branches.

Le cas le plus intéressant est celui où tous les cas ne se cassent pas, à mon avis, mais cela n'a peut-être pas été la portée de l'expérience.

Calyth
la source
4

Les instructions switch / case peuvent être généralement plus rapides à 1 niveau, mais lorsque vous commencez à entrer dans 2 ou plus, les instructions switch / case commencent à prendre 2-3 fois plus longtemps que les instructions if / else imbriquées.

Cet article propose des comparaisons de vitesse mettant en évidence les différences de vitesse lorsque de telles instructions sont imbriquées.

Par exemple, selon leurs tests, un exemple de code comme celui-ci:

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 deux fois moins que l'instruction switch / case équivalente a pris pour s'exécuter:

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

Oui, c'est un exemple rudimentaire, mais cela illustre ce point.

Donc, une conclusion pourrait être d'utiliser switch / case pour des types simples qui ne sont qu'un niveau de profondeur, mais pour des comparaisons plus complexes et plusieurs niveaux imbriqués, utilisez les constructions classiques if / else?


la source
-1: 1. L'article a complètement ignoré la prédiction de branche, 2. les algorithmes ne sont pas exactement les mêmes (l'unique if-else sur le lien est déjà codé plus optimisé) et 3. les différences trouvées sont si petites que rien n'excuse l'utilisation d'un code propre et correct (environ 4 ns en 10.000.000 d'appels entre le commutateur et la même construction if-else)
Trojaner
Cet exemple ne sera pas optimisé en raison du peu de cas du bloc de commutation. Généralement, après 5-6 éléments, il générera une table de saut.
antiduh le
0

Le seul avantage du cas if est lorsqu'il y a une augmentation notable de la fréquence d'occurrence du premier cas.

Je ne sais pas exactement où se trouve le seuil, mais j'utilise la syntaxe de cas à moins que le premier "presque toujours" passe le premier test.

Ralph
la source