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_tjest promowany signed intprzez reguły C ++ dotyczące promocji liczb całkowitych, z wyjątkiem platform, na których intnie 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 xi unsigned int ndla 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
rorlub roldla liczby zmiennych.
- icc: obsługiwane dla rotacji o zmiennej liczbie od ICC13 lub wcześniejszej . Stałe liczenie obraca użycie,
shld edi,edi,7które jest wolniejsze i zajmuje więcej bajtów niż rol edi,7na niektórych procesorach (zwłaszcza AMD, ale także niektórych Intel), gdy BMI2 nie jest dostępne rorx eax,edi,25do 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/ _rotrintrinsics z <intrin.h>x86 (w tym x86-64).
gcc dla ARM używa and r1, r1, #31do 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-ndo 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 ni 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 ORte 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)&31w 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 intdział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ą _rotli _rotl64wewnętrzne , i to samo dla prawego przesunięcia. MSVC wymaga <intrin.h>, podczas gdy gcc wymaga <x86intrin.h>. An #ifdefzajmuje 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:
_rotr8i_rotr16 .
- gcc i icc (nie clang):
<x86intrin.h>zapewnia również __rolb/ __rorbdla 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_rolhilub ...qi, ale obroty 32 i 64-bitowe są definiowane za pomocą shift / lub (bez ochrony przed UB, ponieważ kod ia32intrin.hmusi działać tylko na gcc dla x86). Wydaje się, że GNU C nie ma żadnych __builtin_rotatefunkcji 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 .