GCC prend en charge la commande __builtin_clz(int x)
, qui compte le nombre de zéros non significatifs ( zéros consécutifs les plus significatifs) dans l’argument.
Entre autres choses, 0 est idéal pour implémenter efficacement la fonction lg(unsigned int x)
, qui prend le logarithme en base 2 de x
, arrondissant à 1 :
/** return the base-2 log of x, where x > 0 */ unsigned lg(unsigned x) { return 31U - (unsigned)__builtin_clz(x); }
Cela fonctionne de manière simple – considérons en particulier le cas x == 1
et clz(x) == 31
– alors x == 2^0
donc lg(x) == 0
et 31 - 31 == 0
et nous obtenons le résultat correct. Les valeurs plus élevées de x
fonctionnent de la même manière.
En supposant que la fonction intégrée soit efficacement mise en œuvre, cela se termine bien mieux que les solutions alternatives en C pur .
En l’ bsr
, le décompte des zéros est essentiellement le double de l’instruction bsr
en x86. Cela retourne l’index du bit le plus significatif 2 dans l’argument. Donc, s’il y a 10 zéros en tête, le premier bit est dans le bit 21 de l’argument. En général, nous avons 31 - clz(x) == bsr(x)
et donc bsr
implémente directement notre fonction lg()
souhaitée, sans la partie superflue 31U - ...
En fait, vous pouvez lire entre les lignes et constater que la fonction __builtin_clz
été implémentée avec bsr
à l’esprit: il est défini comme un comportement indéfini si l’argument est égal à zéro, alors que l’opération “zéros au début” est parfaitement définie comme 32 (ou quelle que soit la taille de bit de int
est) avec un argument zéro. Donc, __builtin_clz
a certainement été implémenté avec l’idée d’être mappé efficacement à une instruction bsr
sur x86.
Cependant, en regardant ce que fait GCC, à -O3
avec d’autres options par défaut: cela ajoute une tonne de bric -à- brac :
lg(unsigned int): bsr edi, edi mov eax, 31 xor edi, 31 sub eax, edi ret
La ligne xor edi,31
est effectivement un not edi
pour les 4 derniers bits qui importent, c’est-à-dire 3 par neg edi
qui transforme le résultat de bsr
en clz
. Ensuite, le 31 - clz(x)
est effectué.
Cependant, avec -mtune=haswell
, le code simplifie exactement l’instruction unique attendue de bsr
:
lg(unsigned int): bsr eax, edi ret
Pourquoi est-ce le cas est très peu clair pour moi. L’instruction bsr
existe depuis une vingtaine d’années avant Haswell, et le comportement est, autant que je sache, inchangé. Ce n’est pas juste un problème de réglage pour une arche particulière, car bsr
+ un paquet d’instructions supplémentaires ne sera pas plus rapide qu’un bsr
simple et en outre, l’utilisation de -mtune=haswell
entraîne toujours le code plus lent.
La situation pour les entrées et les sorties 64 bits est même légèrement pire : il y a un movsx
supplémentaire dans le chemin critique qui semble ne rien faire car le résultat de clz
ne sera jamais négatif. De nouveau, la variante -march=haswell
est optimale avec une seule instruction bsr
.
Enfin, vérifions clang
les principaux acteurs dans l’espace compilateur non-Windows, icc
et clang
. icc
ne fait que du mauvais travail et ajoute des éléments redondants comme neg eax; add eax, 31; neg eax; add eax, 31;
neg eax; add eax, 31; neg eax; add eax, 31;
– wtf? clang
fait du bon travail, indépendamment du -march
.
0 Par exemple, numériser une image bitmap pour le premier bit défini.
1 Le logarithme de 0 étant indéfini, appeler notre fonction avec un argument de 0 équivaut à un comportement indéfini .
2 Ici, le bit de poids faible est le 0ème bit et le bit de poids fort est le 31ème.
3 Rappelez-vous que -x == ~x + 1
en complément de deux.
Cela ressemble à un problème connu avec gcc: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=50168