web-dev-qa-db-fra.com

L'ordre des cas dans une instruction switch affecte-t-il les performances?

J'ai un programme de cas switch:

Boîtiers de commutation d'ordre croissant:

int main()
{
        int a, sc = 1;
        switch (sc)
        {
                case 1:
                        a = 1;
                        break;
                case 2:
                        a = 2;
                        break;
        }
}

Assemblage du code:

main:
        Push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], 1
        mov     eax, DWORD PTR [rbp-4]
        cmp     eax, 1
        je      .L3
        cmp     eax, 2
        je      .L4
        jmp     .L2
.L3:
        mov     DWORD PTR [rbp-8], 1
        jmp     .L2
.L4:
        mov     DWORD PTR [rbp-8], 2
        nop
.L2:
        mov     eax, 0
        pop     rbp
        ret

Boîtiers de commutation d'ordre décroissant:

int main()
{
        int a, sc = 1;
        switch (sc)
        {
                case 2:
                        a = 1;
                        break;
                case 1:
                        a = 2;
                        break;
        }
}

Assemblage du code:

main:
        Push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], 1
        mov     eax, DWORD PTR [rbp-4]
        cmp     eax, 1
        je      .L3
        cmp     eax, 2
        jne     .L2
        mov     DWORD PTR [rbp-8], 1
        jmp     .L2
.L3:
        mov     DWORD PTR [rbp-8], 2
        nop
.L2:
        mov     eax, 0
        pop     rbp
        ret

Ici, les cas d'ordre ascendant ont généré plus d'assemblage que l'ordre descendant.

Donc, si j'ai plus de cas de commutateurs, l'ordre des cas affecte-t-il les performances?

29
msc

Vous regardez du code non optimisé, donc l'étudier pour les performances n'est pas très significatif. Si vous regardez le code optimisé pour vos exemples, vous constaterez qu'il ne fait pas du tout les comparaisons! L'optimiseur remarque que la variable de commutateur sc a toujours la valeur 1, Il supprime donc le case 2 Inaccessible.

L'optimiseur voit également que la variable a n'est pas utilisée après son affectation, il supprime donc également le code dans case 1, Laissant main() une fonction vide. Et il supprime la fonction prolog/epilog qui manipule rbp puisque ce registre n'est pas utilisé.

Ainsi, le code optimisé est le même pour l'une ou l'autre version de votre fonction main():

main:
    xor eax, eax
    ret

En bref, pour le code dans la question, peu importe l'ordre dans lequel vous placez les instructions case, car aucun de ce code ne sera généré du tout.

L'ordre case importerait-il dans un exemple plus réel où le code est réellement généré et utilisé? Probablement pas. Notez que même dans votre code généré non optimisé, les deux versions testent les deux valeurs case dans l'ordre numérique, en vérifiant d'abord 1 Puis 2, quel que soit l'ordre dans le code source. De toute évidence, le compilateur effectue un tri, même dans le code non optimisé.

N'oubliez pas de noter les commentaires de Glenn et Lundin: la order des sections case n'est pas le seul changement entre vos deux exemples, le code réel est également différent. Dans l'un d'eux, les valeurs de cas correspondent aux valeurs définies dans a, mais pas dans l'autre.

Les compilateurs utilisent diverses stratégies pour les instructions switch/case en fonction des valeurs réelles utilisées. Ils peuvent utiliser une série de comparaisons comme dans ces exemples, ou peut-être une table de saut. Il peut être intéressant d'étudier le code généré, mais comme toujours, si les performances sont importantes, surveillez vos paramètres d'optimisation et test dans une situation réelle.

57
Michael Geary

Optimisation du compilateur des instructions switch est délicate. Bien sûr, vous devez activer les optimisations (par exemple, essayez de compiler votre code avec gcc -O2 -fverbose-asm -S avec [~ # ~] gcc [~ # ~] et regardez à l'intérieur du .s fichier assembleur). BTW sur vos deux exemples mon GCC 7 sur Debian/Sid/x86-64 donne simplement:

        .type   main, @function
main:
.LFB0:
        .cfi_startproc
# rsp.c:13: }
        xorl    %eax, %eax      #
        ret
        .cfi_endproc

(donc il n'y a aucune trace de switch dans ce code généré)

Si vous avez besoin de comprendre comment un compilateur pourrait optimiser switch, il y a quelques articles sur ce sujet, comme this one.

Si j'ai plus de cas de commutateurs, un ordre de cas affecte-t-il les performances?

Pas en général, si vous utilisez un compilateur d'optimisation et lui demandez d'optimiser. Voir aussi this .

Si cela vous tient tant à cœur (mais ce n'est pas le cas, laissez les micro-optimisations à votre compilateur!), Vous devez comparer, profiler et peut-être étudier le code assembleur généré. BTW, cache misses and allocation de registre pourrait avoir beaucoup plus d'importance que l'ordre de case- s donc je pense que vous ne devrait pas déranger du tout. Gardez à l'esprit la approximation timing timing des ordinateurs récents. Mettez les cases dans l'ordre le plus lisible (pour le développeur suivant travaillant sur cette même source code). Lisez aussi à propos de threaded code . Si vous avez des raisons objectives (liées aux performances) de réorganiser les case- (ce qui est très peu probable et devrait se produire au plus une fois dans votre vie), écrivez quelques bons commentaires expliquant ces raisons.

Si vous vous souciez autant des performances, assurez-vous de benchmark et profile , et choisissez un bon compilateur et utilisez-le avec l'optimisation appropriée les options. Peut-être expérimentez plusieurs différents paramètres optimisation (et peut-être plusieurs compilateurs). Vous voudrez peut-être ajouter -march=native (en plus de -O2 ou -O3). Vous pourriez envisager de compiler et de lier avec -flto -O2 pour activer les optimisations de temps de liaison, etc. Vous pouvez également souhaiter des optimisations profile based .

BTW, de nombreux compilateurs sont énormes logiciels libres projets (en particulier [~ # ~] gcc [~ # ~] et Clang ). Si vous vous souciez autant des performances, vous pouvez patcher le compilateur, l'étendre en ajoutant une étape d'optimisation supplémentaire (par forking le code source, en ajoutant quelques plugin pour GCC ou certains GCC MELT extensions). Cela demande des mois ou des années de travail (notamment pour comprendre les représentations internes et l'organisation de ce compilateur).

(N'oubliez pas de prendre en compte les coûts de développement; dans la plupart des cas, ils coûtent beaucoup plus cher)

17

Les performances dépendraient principalement du nombre de ratés de branche pour un ensemble de données donné, pas tellement du nombre total de cas. Et cela dépend à son tour fortement des données réelles et de la façon dont le compilateur a choisi d'implémenter le commutateur (table de répartition, conditions chaînées, arbre de conditions - je ne sais pas si vous pouvez même contrôler cela à partir de C).

6
Thilo

L'instruction switch est généralement compilée via jump tables et non par de simples comparaisons.

Il n'y a donc aucune perte de performances si vous permutez les déclarations de cas.

Cependant, il est parfois utile de conserver plus de cas dans un ordre consécutif et de ne pas utiliser break/return dans certaines entrées, afin que le flux d'exécution passe au cas suivant et évite de dupliquer le code.

Lorsque les différences de nombres entre les cas number sont grandes d'un cas à l'autre, comme dans case 10: et case 200000: le compilateur ne générera sûrement pas de tables de sauts, car il devrait remplir environ 200K entrées presque toutes avec un pointeur vers le default: case, et dans ce cas il utilisera des comparaisons.

5
alinsoar

Dans les cas où la plupart des étiquettes de cas sont consécutives, les compilateurs traitent souvent des instructions switch pour utiliser des tables de saut plutôt que des comparaisons. Les moyens exacts par lesquels les compilateurs décident quelle forme de saut calculé utiliser (le cas échéant) variera selon les différentes implémentations. Parfois, l'ajout de cas supplémentaires à une instruction switch peut améliorer les performances en simplifiant le code généré par un compilateur (par exemple, si le code utilise les cas 4-11, tandis que les cas 0-3 sont traités par défaut, l'ajout explicite de case 0:; case 1:; case 2:; case 3:; avant le default: peut amener le compilateur à comparer l'opérande à 12 et, si c'est moins, à utiliser une table de saut à 12 éléments. L'omission de ces cas peut amener le compilateur à soustraire 4 avant de comparer la différence à 8, puis à utiliser un tableau à 8 éléments.

Une des difficultés rencontrées pour essayer d'optimiser les instructions de commutation est que les compilateurs savent généralement mieux que les programmeurs comment les performances des différentes approches varieraient en fonction de certaines entrées, mais les programmeurs peuvent mieux connaître que les compilateurs la distribution des entrées qu'un programme recevrait. Étant donné quelque chose comme:

if (x==0)
  y++;
else switch(x)
{
  ...
}

un compilateur "intelligent" pourrait reconnaître que changer le code en:

switch(x)
{
  case 0:
    y++;
    break;
  ...
}

pourrait éliminer une comparaison dans tous les cas où x est différent de zéro, au prix d'un saut calculé lorsque x est nul. Si x n'est pas nul la plupart du temps, ce serait un bon échange. Si x est égal à zéro 99,9% du temps, cependant, cela pourrait être un mauvais échange. Différents auteurs de compilateurs diffèrent quant à la mesure dans laquelle ils essaieront d'optimiser des constructions comme les premières dans les secondes.

5
supercat

Votre question est très simple - votre code n'est pas le même, donc il ne produira pas le même assemblage! Le code optimisé ne dépend pas seulement des instructions individuelles, mais aussi de tout ce qui l'entoure. Et dans ce cas, il est facile d'expliquer l'optimisation.

Dans votre premier exemple, le cas 1 donne a = 1 et le cas 2 donne a = 2. Le compilateur peut optimiser cela pour définir a = sc pour ces deux cas, qui est une seule instruction.

Dans votre deuxième exemple, le cas 1 donne a = 2 et le cas 2 donne a = 1. Le compilateur ne peut plus prendre ce raccourci, il doit donc définir explicitement a = 1 ou a = 2 pour les deux cas. Bien sûr, cela nécessite plus de code.

Si vous avez simplement pris votre premier exemple et échangé l'ordre des cas et le code conditionnel alors vous devriez obtenir le même assembleur.

Vous pouvez tester cette optimisation en utilisant le code

int main()
{
    int a, sc = 1;

    switch (sc)
    {
        case 1:
        case 2:
            a = sc;
            break;
    }
}

qui devrait également donner exactement le même assembleur.

Par ailleurs, votre code de test suppose que sc est réellement lu. La plupart des compilateurs d'optimisation modernes sont capables de détecter que sc ne change pas entre l'affectation et l'instruction switch, et de remplacer la lecture de sc par une valeur constante 1. Une optimisation supplémentaire supprimera ensuite la ou les branches redondantes de l'instruction switch, puis même la l'affectation pourrait être optimisée car un ne change pas réellement. Et du point de vue de la variable a, le compilateur peut également découvrir que a n'est pas lu ailleurs et donc supprimer complètement cette variable du code.

Si vous voulez vraiment que sc soit lu et a défini, vous devez les déclarer tous les deux volatile. Heureusement, le compilateur semble l'avoir implémenté comme vous l'espériez - mais vous ne pouvez absolument pas vous y attendre lorsque l'optimisation est activée.

4
Graham

Vous devriez probablement activer les optimisations pour votre compilateur avant de comparer le code Assembly, mais le problème est que votre variable est connue au moment de la compilation, donc le compilateur peut tout supprimer de votre fonction car elle n'a pas d'effets secondaires.

Cet exemple montre que même si vous changez l'ordre des cas dans une instruction switch dans votre exemple, GCC et la plupart des autres compilateurs les réorganiseront si les optimisations sont activées. J'ai utilisé des fonctions externes pour m'assurer que les valeurs ne sont connues qu'au moment de l'exécution, mais j'aurais également pu utiliser Rand par exemple.

De plus, lorsque vous ajoutez plus de cas, le compilateur peut remplacer les sauts conditionnels par une table qui contient les adresses des fonctions et il sera toujours réorganisé par GCC comme on peut le voir ici .

4
user8880145