Zobacz także wcześniejszą wersję tej odpowiedzi na inne pytanie dotyczące rotacji, zawierającą więcej szczegółów na temat tego, co asm gcc / clang produkuje dla x86.
Najbardziej przyjaznym dla kompilatora sposobem wyrażenia rotacji w C i C ++, który pozwala uniknąć niezdefiniowanego zachowania, wydaje się być implementacja Johna Regehra . Dostosowałem go do obracania się o szerokość typu (używając typów o stałej szerokości, takich jak uint32_t
).
#include <stdint.h> // for uint32_t
#include <limits.h> // for CHAR_BIT
#include <assert.h>
static inline uint32_t rotl32 (uint32_t n, unsigned int c)
{
const unsigned int mask = (CHAR_BIT*sizeof(n) - 1);
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);
c &= mask;
return (n>>c) | (n<<( (-c)&mask ));
}
Działa dla każdego typu liczby całkowitej bez znaku, nie tylko uint32_t
, więc możesz tworzyć wersje dla innych rozmiarów.
Zobacz także wersję szablonu C ++ 11 z wieloma sprawdzeniami bezpieczeństwa (w tym, static_assert
że szerokość typu to potęga 2) , co nie ma miejsca na przykład w przypadku niektórych 24-bitowych DSP lub 36-bitowych komputerów mainframe.
Zalecam używanie szablonu tylko jako zaplecza dla opakowań z nazwami, które jawnie zawierają szerokość obrotu. Reguły promocji liczb całkowitych oznaczają, że rotl_template(u16 & 0x11UL, 7)
obrót wykonywałby 32 lub 64-bitowy, a nie 16 (w zależności od szerokości unsigned long
). Nawet uint16_t & uint16_t
jest promowany signed int
przez reguły C ++ dotyczące promocji liczb całkowitych, z wyjątkiem platform, na których int
nie jest szerszy niż uint16_t
.
Na x86 , ta wersja jest wbudowana do pojedynczegorol r32, cl
(lub rol r32, imm8
) z kompilatorami, które go grokują, ponieważ kompilator wie, że instrukcje obrotu i przesunięcia x86 maskują liczbę przesunięć w taki sam sposób, jak robi to źródło C.
Obsługa kompilatora dla tego idiomu unikania UB na x86, dla uint32_t x
i unsigned int n
dla przesunięć o zmiennej liczbie:
- clang: rozpoznawany jako zmienna liczba obraca się od clang3.5, wiele zmian + lub insns przed tym.
- gcc: rozpoznawane jako zmienna-count obraca się od gcc4.9 , wiele przesunięć + lub insns przed tym. gcc5, a później optymalizuje gałąź i maskę również w wersji wikipedii, używając tylko instrukcji
ror
lub rol
dla liczby zmiennych.
- icc: obsługiwane dla rotacji o zmiennej liczbie od ICC13 lub wcześniejszej . Stałe liczenie obraca użycie,
shld edi,edi,7
które jest wolniejsze i zajmuje więcej bajtów niż rol edi,7
na niektórych procesorach (zwłaszcza AMD, ale także niektórych Intel), gdy BMI2 nie jest dostępne rorx eax,edi,25
do zapisania pliku MOV.
- MSVC: x86-64 CL19: rozpoznawany tylko w przypadku rotacji o stałej liczbie. (Rozpoznawany jest idiom Wikipedii, ale gałąź i AND nie są zoptymalizowane). Użyj
_rotl
/ _rotr
intrinsics z <intrin.h>
x86 (w tym x86-64).
gcc dla ARM używa and r1, r1, #31
do obracania zmiennej count, ale nadal robi rzeczywistego obracają się z pojedynczej instrukcji : ror r0, r0, r1
. Więc gcc nie zdaje sobie sprawy, że rotate-count są z natury modularne. Jak mówi dokumentacja ARM, „ROR z długością przesunięcia n
, więcej niż 32 to to samo, co ROR z długością przesunięcia n-32
” . Myślę, że gcc jest tu zdezorientowany, ponieważ przesunięcia w lewo / w prawo na ARM nasycają licznik, więc przesunięcie o 32 lub więcej wyczyści rejestr. (W przeciwieństwie do x86, gdzie przesunięcia maskują liczbę tak samo, jak obroty). Prawdopodobnie decyduje, że potrzebuje instrukcji AND przed rozpoznaniem idiomu rotacji, z powodu tego, jak przesunięcia niekołowe działają na ten cel.
Obecne kompilatory x86 nadal używają dodatkowej instrukcji do maskowania zmiennej liczby obrotów dla 8 i 16-bitowych obrotów, prawdopodobnie z tego samego powodu, dla którego nie unikają AND na ARM. Jest to pominięta optymalizacja, ponieważ wydajność nie zależy od liczby obrotów na żadnym procesorze x86-64. (Maskowanie zliczeń zostało wprowadzone w 286 ze względu na wydajność, ponieważ obsługiwało zmiany iteracyjnie, a nie ze stałym opóźnieniem, jak w przypadku nowoczesnych procesorów).
Przy okazji, preferuj rotację w prawo dla rotacji ze zmienną liczbą, aby uniknąć zmuszania kompilatora 32-n
do zaimplementowania rotacji w lewo na architekturach takich jak ARM i MIPS, które zapewniają tylko rotację w prawo. (Optymalizuje to zliczaniem stałych czasu kompilacji).
Ciekawostka: ARM tak naprawdę nie ma dedykowanego shift / obracanie wskazówek, to tylko MOV ze źródła argumentu przechodzi beczki-shifter w trybie ROR : mov r0, r0, ror r1
. Więc rotate może złożyć w operand rejestru źródłowego dla instrukcji EOR lub czegoś podobnego.
Upewnij się, że używasz typów bez znaku dla n
i wartości zwracanej, w przeciwnym razie nie będzie to rotacja . (gcc dla celów x86 wykonuje arytmetyczne przesunięcia w prawo, przesuwając kopie bitu znaku zamiast zer, co prowadzi do problemu, gdy OR
te dwie wartości są przesunięte razem. Przesunięcia w prawo ujemnych liczb całkowitych ze znakiem to zachowanie zdefiniowane przez implementację w języku C.)
Upewnij się również, że liczba przesunięć jest typem bez znaku , ponieważ (-n)&31
w przypadku typu ze znakiem może to być uzupełnienie lub znak / wielkość, a nie to samo, co modularne 2 ^ n, które otrzymujesz z uzupełnieniem bez znaku lub do dwóch. (Zobacz komentarze do wpisu na blogu Regehr). unsigned int
działa dobrze na każdym kompilatorze, który oglądałem, dla każdej szerokości x
. Niektóre inne typy faktycznie pokonują rozpoznawanie idiomów dla niektórych kompilatorów, więc nie używaj tylko tego samego typu, co x
.
Niektóre kompilatory zapewniają funkcje wewnętrzne dla rotacji , co jest znacznie lepsze niż inline-asm, jeśli wersja przenośna nie generuje dobrego kodu w kompilatorze, na który jest przeznaczona. Nie ma elementów wewnętrznych dla wielu platform dla żadnych znanych mi kompilatorów. Oto niektóre z opcji x86:
- Dokumenty firmy Intel, które
<immintrin.h>
zapewniają _rotl
i _rotl64
wewnętrzne , i to samo dla prawego przesunięcia. MSVC wymaga <intrin.h>
, podczas gdy gcc wymaga <x86intrin.h>
. An #ifdef
zajmuje się gcc vs. icc, ale clang nie wydaje się ich dostarczać nigdzie, z wyjątkiem trybu zgodności MSVC z-fms-extensions -fms-compatibility -fms-compatibility-version=17.00
. A asm, który dla nich emituje, jest do bani (dodatkowe maskowanie i CMOV).
- MSVC:
_rotr8
i_rotr16
.
- gcc i icc (nie clang):
<x86intrin.h>
zapewnia również __rolb
/ __rorb
dla 8-bitowego obrotu w lewo / w prawo, __rolw
/ __rorw
(16-bitowy), __rold
/ __rord
(32-bitowy), __rolq
/ __rorq
(64-bitowy, zdefiniowany tylko dla 64-bitowych celów). W przypadku wąskich obrotów implementacja używa __builtin_ia32_rolhi
lub ...qi
, ale obroty 32 i 64-bitowe są definiowane za pomocą shift / lub (bez ochrony przed UB, ponieważ kod ia32intrin.h
musi działać tylko na gcc dla x86). Wydaje się, że GNU C nie ma żadnych __builtin_rotate
funkcji wieloplatformowych, jak to ma miejsce __builtin_popcount
(co rozszerza się na wszystko, co jest optymalne na platformie docelowej, nawet jeśli nie jest to pojedyncza instrukcja). W większości przypadków dobry kod uzyskuje się dzięki rozpoznawaniu idiomów.
#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 _rotl(x, n);
}
#endif
Prawdopodobnie niektóre kompilatory inne niż x86 również mają wewnętrzne elementy, ale nie rozszerzajmy tej odpowiedzi społeczności wiki, aby uwzględnić je wszystkie. (Może zrób to w istniejącej odpowiedzi na temat wewnętrznych elementów ).
(Stara wersja tej odpowiedzi sugerowała wbudowany asm specyficzny dla MSVC (który działa tylko dla 32-bitowego kodu x86) lub http://www.devx.com/tips/Tip/14043 dla wersji C. Komentarze odpowiadają na to .)
Asm inline pokonuje wiele optymalizacji , zwłaszcza w stylu MSVC, ponieważ wymusza przechowywanie / ponowne ładowanie danych wejściowych . Starannie napisana rotacja inline-asm GNU C pozwoliłaby licznikowi być natychmiastowym operandem dla zliczeń przesunięć stałych w czasie kompilacji, ale nadal nie można było całkowicie zoptymalizować, jeśli wartość, która ma zostać przesunięta, jest również stałą czasu kompilacji po inliningu. https://gcc.gnu.org/wiki/DontUseInlineAsm .