Voir également une version antérieure de cette réponse sur une autre question de rotation avec quelques détails supplémentaires sur ce que produit asm gcc / clang pour x86.
Le moyen le plus convivial pour le compilateur d'exprimer une rotation en C et C ++ qui évite tout comportement indéfini semble être l'implémentation de John Regehr . Je l'ai adapté pour qu'il tourne selon la largeur du type (en utilisant des types à largeur fixe comme uint32_t
).
#include <stdint.h> // for uint32_t
#include <limits.h> // for CHAR_BIT
// #define NDEBUG
#include <assert.h>
static inline uint32_t rotl32 (uint32_t n, unsigned int c)
{
const unsigned int mask = (CHAR_BIT*sizeof(n) - 1); // assumes width is a power of 2.
// assert ( (c<=mask) &&"rotate by type width or more");
c &= mask;
return (n<<c) | (n>>( (-c)&mask ));
}
static inline uint32_t rotr32 (uint32_t n, unsigned int c)
{
const unsigned int mask = (CHAR_BIT*sizeof(n) - 1);
// assert ( (c<=mask) &&"rotate by type width or more");
c &= mask;
return (n>>c) | (n<<( (-c)&mask ));
}
Fonctionne pour tout type entier non signé, pas seulement uint32_t
, vous pouvez donc créer des versions pour d'autres tailles.
Voir aussi une version de modèle C ++ 11 avec de nombreuses vérifications de sécurité (y compris un fait static_assert
que la largeur du type est une puissance de 2) , ce qui n'est pas le cas sur certains DSP 24 bits ou mainframes 36 bits, par exemple.
Je recommanderais uniquement d'utiliser le modèle comme back-end pour les wrappers avec des noms qui incluent explicitement la largeur de rotation. Les règles de promotion d'entiers signifient que rotl_template(u16 & 0x11UL, 7)
cela ferait une rotation 32 ou 64 bits, et non 16 (selon la largeur de unsigned long
). Even uint16_t & uint16_t
est promu signed int
par les règles de promotion d'entiers de C ++, sauf sur les plates-formes où int
n'est pas plus large que uint16_t
.
Sur x86 , cette version s'aligne sur un seulrol r32, cl
(ou rol r32, imm8
) avec des compilateurs qui le grokent, car le compilateur sait que les instructions de rotation et de décalage x86 masquent le nombre de décalage de la même manière que la source C.
Prise en charge du compilateur pour cet idiome évitant UB sur x86, pour uint32_t x
et unsigned int n
pour les décalages à nombre variable:
- clang: reconnu pour les rotations à nombre variable depuis clang3.5, plusieurs décalages + ou insns avant cela.
- gcc: reconnu pour la rotation du nombre de variables depuis gcc4.9 , plusieurs décalages + ou insns avant cela. gcc5 et les versions ultérieures optimisent également la branche et le masque dans la version wikipedia, en utilisant simplement une instruction
ror
ou rol
pour le nombre de variables.
- icc: pris en charge pour les rotations à nombre variable depuis ICC13 ou antérieur . Le nombre constant tourne l'utilisation
shld edi,edi,7
qui est plus lente et prend plus d'octets que rol edi,7
sur certains processeurs (en particulier AMD, mais aussi certains Intel), lorsque BMI2 n'est pas disponible pour rorx eax,edi,25
enregistrer un MOV.
- MSVC: x86-64 CL19: reconnu uniquement pour les rotations à comptage constant. (L'idiome wikipedia est reconnu, mais la branche et AND ne sont pas optimisés). Utilisez les
_rotl
/ _rotr
intrinsèques de<intrin.h>
de x86 (y compris x86-64).
gcc pour ARM utilise un and r1, r1, #31
pour tourne-count variable, mais fait encore la rotation réelle avec une seule instruction : ror r0, r0, r1
. Donc gcc ne se rend pas compte que le nombre de tours est intrinsèquement modulaire. Comme le dit ARM, "ROR avec longueur de décalage n
, plus de 32 est identique à ROR avec longueur de décalage n-32
" . Je pense que gcc est confus ici parce que les décalages gauche / droite sur ARM saturent le compte, donc un décalage de 32 ou plus effacera le registre. (Contrairement à x86, où les décalages masquent le nombre de la même manière que les rotations). Il décide probablement qu'il a besoin d'une instruction AND avant de reconnaître l'idiome de rotation, en raison de la façon dont les décalages non circulaires fonctionnent sur cette cible.
Les compilateurs x86 actuels utilisent toujours une instruction supplémentaire pour masquer un nombre de variables pour les rotations 8 et 16 bits, probablement pour la même raison qu'ils n'évitent pas le AND sur ARM. Il s'agit d'une optimisation manquée, car les performances ne dépendent pas du nombre de rotations sur n'importe quel processeur x86-64. (Le masquage des décomptes a été introduit avec 286 pour des raisons de performances car il gérait les changements de manière itérative, pas avec une latence constante comme les processeurs modernes.)
BTW, préférez la rotation à droite pour les rotations à nombre variable, pour éviter de forcer le compilateur 32-n
à implémenter une rotation à gauche sur des architectures comme ARM et MIPS qui ne fournissent qu'une rotation à droite. (Cela s'optimise avec des comptages constants de temps de compilation.)
Fait amusant: ARM n'a pas vraiment d'instructions de changement / rotation dédiées, c'est juste MOV avec le Opérande source en passant par le canon-levier de vitesses en mode ROR : mov r0, r0, ror r1
. Ainsi, une rotation peut se plier en un opérande source de registre pour une instruction EOR ou quelque chose.
Assurez-vous d'utiliser des types non signés pour n
et la valeur de retour, sinon ce ne sera pas une rotation . (gcc pour les cibles x86 effectue des décalages arithmétiques vers la droite, se déplaçant en copies du bit de signe plutôt que des zéros, ce qui entraîne un problème lorsque vous OR
déplacez les deux valeurs ensemble. Les décalages à droite des entiers signés négatifs sont un comportement défini par l'implémentation en C.)
Assurez-vous également que le nombre de décalages est un type non signé , car (-n)&31
avec un type signé peut être le complément à un ou le signe / la magnitude, et pas le même que le 2 ^ n modulaire que vous obtenez avec un complément non signé ou à deux. (Voir les commentaires sur le billet de blog de Regehr). unsigned int
fonctionne bien sur tous les compilateurs que j'ai consultés, pour chaque largeur dex
. Certains autres types détruisent la reconnaissance d'idiomes pour certains compilateurs, alors n'utilisez pas simplement le même type que x
.
Certains compilateurs fournissent des éléments intrinsèques pour les rotations , ce qui est bien meilleur que inline-asm si la version portable ne génère pas un bon code sur le compilateur que vous ciblez. Il n'y a pas d'intrinsèques multiplateformes pour les compilateurs que je connaisse. Voici quelques-unes des options x86:
- Documents Intel qui
<immintrin.h>
fournissent _rotl
et _rotl64
intrinsèques , et même pour le décalage à droite. MSVC nécessite <intrin.h>
, tandis que gcc nécessite <x86intrin.h>
. An #ifdef
s'occupe de gcc contre icc, mais clang ne semble pas les fournir nulle part, sauf en mode de compatibilité MSVC avec-fms-extensions -fms-compatibility -fms-compatibility-version=17.00
. Et l'asm qu'il émet pour eux est nul (masquage supplémentaire et CMOV).
- MSVC:
_rotr8
et_rotr16
.
- gcc et icc (not clang):
<x86intrin.h>
fournit également __rolb
/ __rorb
pour une rotation 8 bits gauche / droite, __rolw
/ __rorw
(16 bits), __rold
/ __rord
(32 bits), __rolq
/ __rorq
(64 bits, défini uniquement pour les cibles 64 bits). Pour les rotations étroites, l'implémentation utilise __builtin_ia32_rolhi
ou ...qi
, mais les rotations 32 et 64 bits sont définies à l'aide de shift / or (sans protection contre UB, car le code ia32intrin.h
ne doit fonctionner que sur gcc pour x86). GNU C semble ne pas avoir de __builtin_rotate
fonctions multiplateformes comme il le fait __builtin_popcount
(qui s'étend à ce qui est optimal sur la plate-forme cible, même si ce n'est pas une seule instruction). La plupart du temps, vous obtenez un bon code grâce à la reconnaissance d'idiomes.
// For real use, probably use a rotate intrinsic for MSVC, or this idiom for other compilers. This pattern of #ifdefs may be helpful
#if defined(__x86_64__) || defined(__i386__)
#ifdef _MSC_VER
#include <intrin.h>
#else
#include <x86intrin.h> // Not just <immintrin.h> for compilers other than icc
#endif
uint32_t rotl32_x86_intrinsic(rotwidth_t x, unsigned n) {
//return __builtin_ia32_rorhi(x, 7); // 16-bit rotate, GNU C
return _rotl(x, n); // gcc, icc, msvc. Intel-defined.
//return __rold(x, n); // gcc, icc.
// can't find anything for clang
}
#endif
Vraisemblablement, certains compilateurs non-x86 ont aussi des éléments intrinsèques, mais n'élargissons pas cette réponse de communauté-wiki pour les inclure tous. (Peut-être faites-le dans la réponse existante sur les intrinsèques ).
(L'ancienne version de cette réponse suggérait un asm en ligne spécifique à MSVC (qui ne fonctionne que pour le code x86 32 bits), ou http://www.devx.com/tips/Tip/14043 pour une version C. Les commentaires répondent à cela .)
Inline asm défait de nombreuses optimisations , en particulier de style MSVC car il force les entrées à être stockées / rechargées . Une rotation en ligne-asm GNU C soigneusement écrite permettrait au compte d'être un opérande immédiat pour les décomptes de décalage à constante de temps de compilation, mais il ne pourrait toujours pas s'optimiser entièrement si la valeur à déplacer est également une constante de temps de compilation après l'inlining. https://gcc.gnu.org/wiki/DontUseInlineAsm .