Pourquoi un commutateur n'est-il pas optimisé de la même manière que chaîné s'il en est autrement en c / c ++?

39

L'implémentation suivante de square produit une série d'instructions cmp / je comme j'attendrais d'une instruction if chaînée:

int square(int num) {
    if (num == 0){
        return 0;
    } else if (num == 1){
        return 1;
    } else if (num == 2){
        return 4;
    } else if (num == 3){
        return 9;
    } else if (num == 4){
        return 16;
    } else if (num == 5){
        return 25;
    } else if (num == 6){
        return 36;
    } else if (num == 7){
        return 49;
    } else {
        return num * num;
    }
}

Et ce qui suit produit une table de données pour le retour:

int square_2(int num) {
    switch (num){
        case 0: return 0;
        case 1: return 1;
        case 2: return 4;
        case 3: return 9;
        case 4: return 16;
        case 5: return 25;
        case 6: return 36;
        case 7: return 49;
        default: return num * num;
    }
}

Pourquoi gcc est-il incapable d'optimiser celui du haut dans celui du bas?

Démontage pour référence: https://godbolt.org/z/UP_igi

EDIT: intéressant, MSVC génère une table de saut au lieu d'une table de données pour le boîtier de commutation. Et étonnamment, clang les optimise pour le même résultat.

chacham15
la source
3
Que voulez-vous dire par "comportement indéfini"? Tant que le comportement observable est le même, le compilateur peut générer le code assembleur / machine qu'il veut
bolov
2
@ user207421 ignorant le returns; les cas n'ont pas breaks, donc le commutateur a également un ordre d'exécution spécifique. La chaîne if / else a des retours dans chaque branche, la sémantique dans ce cas est équivalente. L'optimisation n'est pas impossible . Comme contre-exemple icc n'optimise aucune des fonctions.
user1810087
9
Peut-être que la réponse la plus simple ... gcc n'est tout simplement pas en mesure de voir cette structure et de l'optimiser (encore).
user1810087
3
Je suis d'accord avec @ user1810087. Vous avez simplement trouvé la limite actuelle du processus de raffinement du compilateur. Un sous-sous-cas qui n'est actuellement pas reconnu comme optimisable (par certains compilateurs). En fait, toutes les autres chaînes if ne peuvent pas être optimisées de cette façon, mais uniquement le sous-ensemble dans lequel la variable SAME est testée par rapport à des valeurs constantes.
Roberto Caboni
1
Le if-else a un ordre d'exécution différent, de haut en bas. Pourtant, le remplacement du code par des instructions juste si n'a pas amélioré le code machine. Le commutateur, d'autre part, n'a pas d'ordre d'exécution prédéfini et n'est essentiellement qu'une table de saut goto glorifiée. Cela étant dit, un compilateur est autorisé à raisonner sur le comportement observable ici, donc la mauvaise optimisation de la version if-else est assez décevante.
Lundin

Réponses:

29

Le code généré pour switch-caseutilise classiquement une table de saut. Dans ce cas, le retour direct via une table de correspondance semble être une optimisation utilisant le fait que chaque cas implique ici un retour. Bien que la norme ne donne aucune garantie à cet effet, je serais surpris si un compilateur générait une série de comparaisons au lieu d'une table de saut pour un commutateur classique.

Maintenant if-else, c'est exactement le contraire. Tandis qu'il switch-cases'exécute en temps constant, quel que soit le nombre de branches, if-elseest optimisé pour un plus petit nombre de branches. Ici, vous vous attendez à ce que le compilateur génère essentiellement une série de comparaisons dans l'ordre où vous les avez écrites.

Donc, si je l'avais utilisé if-elseparce que je m'attends à ce que la plupart des appels square()soient pour 0ou 1et rarement pour d'autres valeurs, alors `` l'optimisation '' en une recherche dans la table pourrait en fait faire fonctionner mon code plus lentement que prévu, ce qui irait à l'encontre de mon objectif d'utiliser un à la ifplace d'unswitch . Donc, bien que cela soit discutable, je pense que GCC fait la bonne chose et Clang est trop agressif dans son optimisation.

Quelqu'un avait, dans les commentaires, partagé un lien où clang fait cette optimisation et génère également du code basé sur une table de recherche if-else. Quelque chose de notable se produit lorsque nous réduisons le nombre de cas à seulement deux (et une valeur par défaut) avec clang. Il génère une fois de plus un code identique pour if et switch, mais cette fois, bascule vers compare et se déplace au lieu de l'approche de table de recherche, pour les deux. Cela signifie que même le cliquet favorable aux commutateurs sait que le motif «si» est plus optimal lorsque le nombre de cas est petit!

En résumé, une séquence de comparaisons pour if-elseet une table de saut pour switch-caseest le modèle standard que les compilateurs ont tendance à suivre et que les développeurs ont tendance à attendre lorsqu'ils écrivent du code. Cependant, dans certains cas particuliers, certains compilateurs peuvent choisir de rompre ce modèle lorsqu'ils estiment qu'il offre une meilleure optimisation. D'autres compilateurs pourraient simplement choisir de s'en tenir au modèle de toute façon, même s'ils ne sont apparemment pas optimaux, en faisant confiance au développeur pour savoir ce qu'il veut. Les deux sont des approches valables avec leurs propres avantages et inconvénients.

th33lf
la source
2
Oui, l'optimisation est une épée à plusieurs tranchants: ce qu'ils écrivent, ce qu'ils veulent, ce qu'ils obtiennent et qui nous maudissons pour cela.
Déduplicateur
1
"... alors 'optimiser' ceci en une recherche de table entraînerait en fait mon code à s'exécuter plus lentement que prévu ..." Pouvez-vous fournir une justification à cela? Pourquoi une table de saut serait-elle plus lente que deux branches conditionnelles possibles (pour vérifier les entrées par rapport à 0et 1)?
Cody Gray
@CodyGray Je dois avouer que je n'ai pas atteint le niveau des cycles de comptage - je viens de penser que la charge de la mémoire via un pointeur peut prendre plus de cycles qu'une comparaison et un saut, mais je peux me tromper. Cependant, j'espère que vous serez d'accord avec moi que même dans ce cas, au moins pour «0», ifest évidemment plus rapide? Maintenant, voici un exemple d'une plate-forme où 0 et 1 seraient plus rapides lors de l'utilisation ifque lors de l'utilisation de switch: godbolt.org/z/wcJhvS (Notez qu'il existe également plusieurs autres optimisations en jeu ici)
th33lf
1
De toute façon, le comptage des cycles ne fonctionne pas sur les architectures OOO superscalaires modernes. :-) Les charges à partir de la mémoire ne seront pas plus lentes que les branches mal prédites, la question est donc de savoir quelle est la probabilité de prédire la branche? Cette question s'applique à toutes sortes de branches conditionnelles, qu'elles soient générées par des ifinstructions explicites ou automatiquement par le compilateur. Je ne suis pas un expert en ARM, donc je ne suis pas vraiment sûr de savoir si vous prétendez switchêtre plus rapide que ce qui ifest vrai. Cela dépendrait de la pénalité pour les branches mal prédites, et cela dépendrait en fait de quel ARM.
Cody Gray
0

Une justification possible est que si des valeurs faibles de numsont plus probables, par exemple toujours 0, le code généré pour le premier peut être plus rapide. Le code généré pour le commutateur prend un temps égal pour toutes les valeurs.

Comparaison des meilleurs cas, selon ce tableau . Voir cette réponse pour l'explication du tableau.

Si num == 0 , pour "si" vous avez xor, test, je (avec saut), ret. Latence: 1 + 1 + saut. Cependant, xor et test sont indépendants de sorte que la vitesse d'exécution réelle serait plus rapide que 1 + 1 cycles.

Si num < 7 , pour "switch", vous avez mov, cmp, ja (sans saut), mov, ret. Latence: 2 + 1 + pas de saut + 2.

Une instruction de saut qui ne se traduit pas par un saut est plus rapide qu'une instruction qui résulte d'un saut. Cependant, le tableau ne définit pas la latence pour un saut, il n'est donc pas clair pour moi lequel est le meilleur. Il est possible que le dernier soit toujours meilleur et que GCC ne soit tout simplement pas en mesure de l'optimiser.

vll
la source
1
Hmm, théorie intéressante, mais pour ifs vs switch vous avez: xor, test, jmp vs mov, cmp jmp. Trois instructions chacune, la dernière étant un saut. Semble égal dans le meilleur des cas, non?
chacham15
3
Msgstr "Une instruction de saut qui ne résulte pas en saut est plus rapide que celle qui résulte en saut.". C'est la prédiction de branche qui compte.
geza