Je travaillais sur un projet personnel récemment lorsque je suis tombé sur un problème étrange.
Dans une boucle très serrée, j'ai un entier avec une valeur comprise entre 0 et 15. J'ai besoin d'obtenir -1 pour les valeurs 0, 1, 8 et 9 et 1 pour les valeurs 4, 5, 12 et 13.
Je me suis tourné vers godbolt pour vérifier quelques options et j'ai été surpris de constater que le compilateur ne pouvait pas optimiser une instruction switch de la même manière qu'une chaîne if.
Le lien est ici: https://godbolt.org/z/WYVBFl
Le code est:
const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
int a(int num) {
return lookup[num & 0xF];
}
int b(int num) {
num &= 0xF;
if (num == 0 || num == 1 || num == 8 || num == 9)
return -1;
if (num == 4 || num == 5 || num == 12 || num == 13)
return 1;
return 0;
}
int c(int num) {
num &= 0xF;
switch (num) {
case 0: case 1: case 8: case 9:
return -1;
case 4: case 5: case 12: case 13:
return 1;
default:
return 0;
}
}
J'aurais pensé que b et c produiraient les mêmes résultats, et j'espérais pouvoir lire les bit-hacks pour trouver une implémentation efficace moi-même puisque ma solution (l'instruction switch - sous une autre forme) était assez lente.
Curieusement, b
compilé en bit-hacks alors qu'il c
était à peu près non optimisé ou réduit à un cas différent en a
fonction du matériel cible.
Quelqu'un peut-il expliquer pourquoi il y a cet écart? Quelle est la manière «correcte» d'optimiser cette requête?
ÉDITER:
Clarification
Je veux que la solution de commutation soit la plus rapide ou une solution similaire "propre". Cependant, lorsqu'elle est compilée avec des optimisations sur ma machine, la solution if est beaucoup plus rapide.
J'ai écrit un programme rapide pour démontrer et TIO a les mêmes résultats que je trouve localement: Essayez-le en ligne!
Avec static inline
la table de recherche accélère un peu: essayez-le en ligne!
la source
-O3
, et il a compiléc
en quelque chose de probablement pire quea
oub
(c
avait deux sauts conditionnels plus quelques manipulations de bits, contre un seul saut conditionnel et une manipulation de bits plus simple pourb
), mais toujours mieux que des tests élément par élément. Je ne sais pas vraiment ce que vous demandez ici; le simple fait est qu'un compilateur d'optimisation peut transformer n'importe lequel de ces éléments en l' un des autres s'il le souhaite, et il n'y a pas de règles strictes pour ce qu'il fera ou ne fera pas.if
bat toujoursswitch
(la recherche étrange devient encore plus rapide) [TIO à suivre]Réponses:
Si vous énumérez explicitement tous les cas, gcc est très efficace:
est simplement compilé dans une simple branche indexée:
Notez que s'il
default:
n'est pas commenté, gcc revient à sa version de branche imbriquée.la source
pslld
/psrad
ou leurs équivalents AVX2 à 8 voies. Cela dépend beaucoup des autres particularités de votre code.Les compilateurs C ont des cas particuliers pour
switch
, car ils s'attendent à ce que les programmeurs comprennent l'idiome deswitch
et l'exploitent.Code comme:
ne passerait pas l'examen par les codeurs C compétents; trois ou quatre examinateurs s'exclamaient simultanément "cela devrait être un
switch
!"Cela ne vaut pas la peine pour les compilateurs C d'analyser la structure des
if
instructions pour la conversion en table de saut. Les conditions doivent être justes et la quantité de variation possible dans un tas deif
déclarations est astronomique. L'analyse est à la fois compliquée et susceptible de se révéler négative (comme dans: "non, nous ne pouvons pas convertir cesif
s enswitch
").la source
if
plus possible.static
et utilisez les initialiseurs désignés par C99 si vous voulez rendre un peu plus clair ce que vous attribuez, et c'est clairement parfaitement bien.if
(voir modifier). @R .. J'ai élaboré la solution complète au niveau du bit pour le compilateur, ce que j'utilise pour l'instant. Malheureusement dans mon cas, ce sont desenum
valeurs, pas des entiers nus, donc les hacks au niveau du bit ne sont pas très maintenables.Le code suivant calculera votre recherche sans branche, sans LUT, en ~ 3 cycles d'horloge, ~ 4 instructions utiles et ~ 13 octets de
inline
code machine x86 très performant.Cela dépend de la représentation entière du complément à 2.
Vous devez cependant vous assurer que les
u32
ets32
typedefs pointent vraiment vers des types entiers non signés et signés 32 bits.stdint.h
typesuint32_t
etint32_t
aurait été approprié mais je n'ai aucune idée si l'en-tête est disponible pour vous.Voyez par vous-même ici: https://godbolt.org/z/AcJWWf
Sur la sélection de la constante
Votre recherche porte sur 16 très petites constantes comprises entre -1 et +1 inclus. Chacun tient dans 2 bits et il y en a 16, que nous pouvons présenter comme suit:
En les plaçant avec l'index 0 le plus proche du bit le plus significatif, un seul décalage de
2*num
placera le bit de signe de votre nombre à 2 bits dans le bit de signe du registre. Décaler à droite le nombre de 2 bits de 32-2 = signe de 30 bits le prolonge au maximumint
, complétant l'astuce.la source
magic
commentaire expliquant comment le régénérer. Pourriez-vous expliquer comment vous en êtes arrivé à cela?!!(12336 & (1<<x))-!!(771 & (1<<x));
Vous pouvez créer le même effet en utilisant uniquement l'arithmétique:
Même si, techniquement, il s'agit toujours d'une recherche (au niveau du bit).
Si ce qui précède semble trop mystérieux, vous pouvez également faire:
la source