Odwracanie kolejności w SSE/AVX
• tech • 1033 słowa • 5 minut czytania
W ostatniej notce, o możliwościach zmiany kolejności uporządkowania bajtów i bibliotece ByteOrder, wspomniałem, że w wolnej chwili postaram pobawić się SSE i spróbować, w prosty i w miarę wydajny sposób, zaimplementować operację bswap
znaną z 32/64-bitowych rejestrów na rejestrach 128-bitowych, a nawet 256-bitowych.
Poniekąd udało mi się to zrobić ;)
SSE2
Druga wersja strumieniowych rozszerzeń SMID dodała 144 nowych instrukcji do SSE. Kilka z nowych instrukcji całkowitoliczbowych, potrafiących zmieniać kolejność 16- i 32-bitowych elementów w 128-bitowym wektorze oraz instrukcji logicznych, będących w rzeczywistości rozszerzeniami instrukcji MMX dla wektorów XMM, mogą nam posłużyć do implementacji funkcji bswap128
.
Wykorzystując możliwości SSE2, operację bswap
dla 128-bitowego wektora można zapisać jako:
bswap128 proc
; n = xmm0
pshufd xmm0, xmm0, 00011011b ; swap order of dwords
pshuflw xmm0, xmm0, 10110001b ; swap order of words
pshufhw xmm0, xmm0, 10110001b
movdqa xmm1, xmm0 ; swap order of bytes
psrlw xmm0, 8
psllw xmm1, 8
por xmm0, xmm1
ret 0
bswap128 endp
Operand order
dla pshufd
, pshuflw
i pshufhw
podano w formie binarnej, co powinno ułatwić zrozumienie działania. Instrukcje te działają w podobny sposób - kopiują z operandu źródłowego element do operandu docelowego w miejsce określone wartością operandu order
, którego każde 2 bity wyznaczają miejsce kolejnego elementu.
Działanie tych instrukcji adekwatne jest poniższemu kodowi:
dest[0] = src[order & 0x03];
dest[1] = src[(order >> 2) & 0x03];
dest[2] = src[(order >> 4) & 0x03];
dest[3] = src[(order >> 6) & 0x03];
Instrukcje różnią się jedynie sposobem traktowania elementów i miejscem przeznaczenia, pshufd
operuje na 32-bitowych elementach - dword
, natomiast pshuflw
i pshufhw
na 16-bitowych - word
, kolejno w dolnej i górnej połówce wektora.
W roli pshufd
moglibyśmy również wykorzystać rozkaz shufps
z SSE, który w takiej konfiguracji funkcjonalnie tak samo przetasuje elementy w wektorze.
Operacja bswap
przedstawiona wyżej, można zaimplementować na instrukcjach intrinsic:
inline __m128i bswap128(__m128i n) {
n = _mm_shuffle_epi32(n, _MM_SHUFFLE(0, 1, 2, 3));
n = _mm_shufflelo_epi16(n, _MM_SHUFFLE(2, 3, 0, 1));
n = _mm_shufflehi_epi16(n, _MM_SHUFFLE(2, 3, 0, 1));
n = _mm_or_si128(
_mm_srli_epi16(n, 8),
_mm_slli_epi16(n, 8)
);
return n;
}
Kod całkiem zgrabny, ale idźmy dalej, będzie jeszcze lepiej!
SSE3
Korzystając z rozszerzeń SSE3, a dokładniej jego uzupełnienia SSSE3, które wprowadziło 16 nowych rozkazów, jako uzupełnienie 13 instrukcji z SSE3, możemy za pomocą instrukcji pshufb
w łatwy sposób zamienić kolejność bajtów w rejestrze xmm
. Instrukcja ta rozmieszcza elementy w wektorze bajtów zgodnie ze wzorcem zapisanym w wektorze źródłowym, jej działanie jest adekwatne algorytmowi:
for (int i = 0; i < 16; i++)
data[i] = (mask[i] & 0x80) == 0 ? data[mask[i] & 0x0F] : 0;
A zatem, na bazie rozkazu pshufb
możemy naszą funkcję swapującą zapisać jeszcze prościej:
.data
align 16
bswap128@mask dd 0C0D0E0Fh, 08090A0Bh, 04050607h, 00010203h
.code
bswap128 proc
; n = xmm0
pshufb xmm0, xmmword ptr bswap128@mask
ret 0
bswap128 endp
Warto tutaj pokusić się o definicję makra zamiast procedury ;)
Odpowiednikiem powyższego kodu z wykorzystaniem instrukcji intrinsic jest kod zapisany w postaci:
inline __m128i bswap128(__m128i n) {
const static __int8 mask[] = { 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
return _mm_shuffle_epi8(n, *reinterpret_cast<const __m128i*>(mask));
}
W porównaniu do wersji SSE2, dzięki instrukcji pshufb
odwracanie kolejności bajtów w rejestrze xmm
, stało się równie proste jak rozkaz bswap
operujący na 32/64-bitowych rejestrach.
AVX
Advanced Vector Extensions rozszerza zestaw instrukcji SSE o kolejne nowości - rejestry 256-bitowe i nowe instrukcje na nich operujące, a także rozszerza niektóre dotychczasowe instrukcje SSE do działania z rejestrami ymm
.
Implementacje przedstawione wyżej dla SSE2 i SSE3 można również zastosować na 256-bitowych rejestrach ymm
, AVX rozszerza używane tam rozkazy o taka możliwość, wystarczy dodać prefiks v
do tych mnemoników. W takim wypadku należy pamiętać, że vpshufd
nadal używa 8-bitowej maski, a vpshufb
działa równolegle na dolnej i górnej połówce wektora, więc nie uda nam się jednym zamachem przetasować elementów według oczekiwań. Musimy jeszcze przed lub po wykonaniu tych operacji zamienić miejscami dolną i górną część. Dokonać to możemy za pomocą vperm2i128
:
vperm2i128 ymm0, ymm0, ymm0, 00100001b
lub odpowiedniego intrinsic:
n = _mm256_permute2x128_si256(n, n, _MM_SHUFFLE(0,2,0,1));
Tandem vperm2i128
i vpshufd
w wersji opartej na SSE2 można zastąpić jedną z nowo dodanych instrukcji AVX służących do permutacji elementów wektora 256-bitowego. Nas najbardziej będzie interesował rozkaz operujący na elementach o najmniejszym rozmiarze, czyli vpermd
(_mm256_permutevar8x32_epi32
) traktujący wektor jako zbiór 8 elementów 32-bitowych.
Instrukcja ta wykonuje poniższy kod:
for (int i = 0; i < 8; i++)
dest[i] = src[order[i] & 0x07];
Wykorzystując dodatkowo możliwość używania dodatkowego argumentu wyjściowego wprowadzonego przez AXV dla rozkazów SSE, możemy wersje z SSE2 zapisać w nowej postaci:
.data
align 16
bswap256@order dd 07h, 06h, 05h, 04h, 03h, 02h, 01h, 00h
.code
bswap256 proc
; n = ymm0
vmovdqa ymm1, ymmword ptr bswap256@order
vpermd ymm0, ymm1, ymm0
vpshuflw ymm0, ymm0, 10110001b
vpshufhw ymm0, ymm0, 10110001b
vpsllw ymm1, ymm0, 8
vpsrlw ymm0, ymm0, 8
vpor ymm0, ymm0, ymm1
ret 0
bswap256 endp
Najrozsądniejsze jednak wydaje się przeportowanie implementacji SSE3 na wersję AVX operującą na wektorach 256-bitowych, czyli skorzystanie z vpshufb
:
.data
align 16
bswap256@mask dd 1C1D1E1Fh, 18191A1Bh, 14151617h, 00010203h,
0C0D0E0Fh, 08090A0Bh, 04050607h, 00010203h
.code
bswap256 proc
; n = ymm0
vperm2i128 ymm0, ymm0, ymm0, 00100001b
vpshufb ymm0, ymm0, ymmword ptr bswap256@mask
ret 0
bswap256 endp
Algorytm reverse order
Przedstawione tutaj i w poprzedniej notce algorytmy, implementacje i sposoby odwracania kolejności bajtów, głownie dotyczyły stricte zmiany kolejności bajtów w danych i liczbach od 16-, aż do 256-bitowych. Rozpatrywano je przede wszystkim pod kątem przydatności w zastosowaniach związanych z problemem uporządkowania bajtów na różnych platformach sprzętowych.
W rzeczywistości są to proste algorytmy odwracania kolejności bajtów w dowolnym zbiorze/tablicy. Mogą być także wykorzystywane do szybkiego i bardzo efektywnego reversowania stringów czy innych elementów w tablicach, wektorach, czy innych spójnych kawałkach pamięci.
Wojciech Muła przeprowadził ciekawe eksperymenty i testy związane z odwracaniem stringów, testując implementacje oparte na standardowym wykorzystaniu pętli, instrukcji bswap
oraz opartych na rozszerzeniach SSE2 i SSE3, bardzo podobnych do tych jakie przedstawiłem wyżej. Wyniki jego testów można znaleźć na jego stronie w notce Speedup reversing table of bytes, wyniki są bardzo ciekawe.
Przy okazji ciekawych rzeczy mogę wspomnieć o niecodzienny wykorzystaniu rozkazu bswap
, jak przedstawił Gynvael Coldwind, można za jego pomocą wykryć emulator bochs lub QEMU.
Zachęcam wszystkich do zabaw w asemblerze i rozszerzeń SSE i AVX, czasem przeprowadzając proste eksperymenty można wiele nowego się dowiedzieć :)
Komentarze (0)