GCC 4.4:Éviter le contrôle de portée sur l'interrupteur on/instruction du cas de gcc?
-
15-09-2020 - |
Question
Ce n'est qu'un problème sur les versions de GCC avant 4.4, cela a été corrigé dans GCC 4.5.
Est-il possible de dire au compilateur que la variable utilisée dans un commutateur convient dans les cas énoncés?En particulier si c'est une petite plage et il y a un saut de la table générée.
extern int a;
main()
{
switch (a & 0x7) { // 0x7 == 111 values are 0-7
case 0: f0(); break;
case 1: f1(); break;
case 2: f2(); break;
case 3: f3(); break;
case 4: f4(); break;
case 5: f5(); break;
case 6: f6(); break;
case 7: f7(); break;
}
}
J'ai essayé utilise xor pour les bits de poids faible (comme dans l'exemple), l'utilisation des enums, à l'aide de gcc_unreachable() en vain.Le code généré vérifie toujours si la variable est à l'intérieur de la gamme, l'ajout d'un inutile branche conditionnelle et en s'éloignant de la table jump code de calcul.
Note:c'est au plus profond de la boucle d'un décodeur, des questions de rendement de manière significative.
Il semble que je ne suis pas la seulement un.
Il n'y a aucun moyen de dire à gcc que la branche par défaut n'est jamais prise, bien qu'il omet la branche par défaut si elle peut prouver que l' la valeur n'est jamais hors de portée fondée sur les conditionnelle des contrôles.
Alors, comment faites-vous pour aider gcc prouver la variable s'adapte et il n'y a pas de branche par défaut dans l'exemple ci-dessus?(Sans l'ajout d'une branche conditionnelle, bien sûr.)
Les mises à jour
C'était sur OS X 10.6 Snow Leopard avec CCAG 4.2 (par défaut à partir de Xcode.) Il n'est pas arrivé avec GCC 4.4/4.3 sous linux (rapporté par Nathon et Jens Gustedt.)
Les fonctions dans l'exemple sont là pour des raisons de lisibilité, pense que ceux qui sont inline ou tout simplement des déclarations.Faire un appel de fonction sur x86 est cher.
Aussi l'exemple, comme mentionné dans la note, appartient à l'intérieur d'une boucle sur des données (big data.)
Le code généré par gcc 4.2/OS X est:
[...] andl $7, %eax cmpl $7, %eax ja L11 mov %eax, %eax leaq L20(%rip), %rdx movslq (%rdx,%rax,4),%rax addq %rdx, %rax jmp *%rax .align 2,0x90 L20: .long L12-L20 .long L13-L20 .long L14-L20 .long L15-L20 .long L16-L20 .long L17-L20 .long L18-L20 .long L19-L20 L19: [...]
Le problème se situe sur
cmp $7, %eax;
ja L11;
OK, je vais avec le laid solution et l'ajout d'un cas particulier pour les versions de gcc 4.4 ci-dessous à l'aide d'une autre version sans interrupteur et l'utilisation de goto et de gcc &&label extensions.
static void *jtb[] = { &&c_1, &&c_2, &&c_3, &&c_4, &&c_5, &&c_6, &&c_7, &&c_8 }; [...] goto *jtb[a & 0x7]; [...] while(0) { c_1: // something break; c_2: // something break; [...] }
Remarque le tableau des étiquettes est statique, il n'est donc pas calculée à chaque appel.
La solution
J'ai essayé de compiler quelque chose de simple et comparable avec -O5 et -fno-inline (mon f0-f7 fonctions ont été banal), et il a généré ce:
8048420: 55 push %ebp ;; function preamble
8048421: 89 e5 mov %esp,%ebp ;; Yeah, yeah, it's a function.
8048423: 83 ec 04 sub $0x4,%esp ;; do stuff with the stack
8048426: 8b 45 08 mov 0x8(%ebp),%eax ;; x86 sucks, we get it
8048429: 83 e0 07 and $0x7,%eax ;; Do the (a & 0x7)
804842c: ff 24 85 a0 85 04 08 jmp *0x80485a0(,%eax,4) ;; Jump table!
8048433: 90 nop
8048434: 8d 74 26 00 lea 0x0(%esi,%eiz,1),%esi
8048438: 8d 45 08 lea 0x8(%ebp),%eax
804843b: 89 04 24 mov %eax,(%esp)
804843e: e8 bd ff ff ff call 8048400
8048443: 8b 45 08 mov 0x8(%ebp),%eax
8048446: c9 leave
Avez-vous essayer de jouer avec les niveaux d'optimisation?
Autres conseils
Vous pourriez peut-être utiliser un tableau de pointeurs de fonction à la place d'un interrupteur ?
#include <stdio.h>
typedef void (*func)(void);
static void f0(void) { printf("%s\n", __FUNCTION__); }
static void f1(void) { printf("%s\n", __FUNCTION__); }
static void f2(void) { printf("%s\n", __FUNCTION__); }
static void f3(void) { printf("%s\n", __FUNCTION__); }
static void f4(void) { printf("%s\n", __FUNCTION__); }
static void f5(void) { printf("%s\n", __FUNCTION__); }
static void f6(void) { printf("%s\n", __FUNCTION__); }
static void f7(void) { printf("%s\n", __FUNCTION__); }
int main(void)
{
const func f[8] = { f0, f1, f2, f3, f4, f5, f6, f7 };
int i;
for (i = 0; i < 8; ++i)
{
f[i]();
}
return 0;
}
Avez-vous essayé de déclarer la switch
variable comme un champ de bits?
struct Container {
uint16_t a:3;
uint16_t unused:13;
};
struct Container cont;
cont.a = 5; /* assign some value */
switch( cont.a ) {
...
}
Espérons que cela fonctionne!
Je n'ai pas essayé, mais je ne suis pas sûr que gcc_unreachable
fait la même chose que __builtin_unreachable
.Googler les deux, gcc_unreachable
semble être conçu comme une comme une affirmation outil pour le développement de GCC lui-même, peut-être avec une branche de prédiction de l'indice inclus, tandis que __builtin_unreachable
rend le programme instantanément non définie — qui sonne comme la suppression du bloc de base, qui est ce que vous voulez.
Peut-être il suffit d'utiliser un default
étiquette pour la première ou la dernière affaire?
Cette question est tout à fait intéressante du point de vue d'oubli d'une optimisation du compilateur qui semble être une évidence pour nous, et j'ai passé beaucoup de temps à essayer de trouver une solution simple et efficace, en grande partie des personnels de la curiosité.
Cela dit, je dois l'avouer Je suis très sceptique que cet enseignement sera jamais entraîner une différence de performances mesurables dans la pratique, en particulier sur un nouveau mac.Si vous avez une quantité importante de données, vous serez dépendant des e/S, et une seule instruction ne sera jamais le goulot d'étranglement.Si vous avez une petite quantité de données, alors vous aurez besoin pour effectuer une beaucoup beaucoup beaucoup des calculs à plusieurs reprises avant d'une seule instruction va devenir un goulot d'étranglement.
Vous le poster du code de montrer qu'il y a vraiment une différence de performance?Ou de décrire le code et les données de votre travail avec?