- A cóż to takiego to SIMD ?! - zapytacie.
Już odpowiadam.
SIMD = Single Instruction, Multiple Data = jedna instrukcja, wiele danych.
Jest to technologia
umożliwiająca jednoczesne wykonywanie tej samej instrukcji na kilku wartościach. Na pewno znany
jest wam co najmniej jeden przykład zastosowania technologii SIMD. Jest to
MultiMedia Extensions,
w skrócie MMX, u Intela, a 3DNow! u AMD.
Innym mniej znanym zastosowaniem jest SSE, które omówię później.
Zaczniemy od omówienia, jak właściwie działa to całe MMX.
Technologia MMX operuje na 8 rejestrach danych, po 64 bity każdy, nazwanych mm0 ... mm7.
Niestety, rejestry te nie są prawdziwymi
(oddzielnymi) rejestrami - są częściami
rejestrów koprocesora (które, jak pamiętamy, mają po 80 bitów każdy).
Pamiętajcie więc, że nie można naraz wykonywać operacji na FPU i MMX/3DNow!.
Rejestry 64-bitowe służą do umieszczania w nich danych spakowanych. Na czym to polega? Zamiast mieć na przykład 32 bity w jednym rejestrze, można mieć dwa razy po 32. Tak więc rejestry mieszczą 2 podwójne słowa (dword, 32 bity) lub 4 słowa (word, 16 bitów) lub aż 8 spakowanych bajtów.
Zajmijmy się omówieniem instrukcji operujących na tych rejestrach.
Instrukcje MMX można podzielić na kilka grup (nie wszystkie instrukcje będą tu wymienione):
MOVD mmi, rej32/mem32
(i=0,...,7) MOVQ mmi, mmj/mem64
(i,j=0,...,7)PADDB
(bajty) / PADDW
(słowa) /
PADDD
(dwordy)PADDSB
(bajty) / PADDSW
(słowa).szarez dadzą w sumie
czarnya nie coś pośrodku skali kolorów.
PADDUSB
/ PADDUSW
.PSUBB
(bajty) / PSUBW
(słowa) /
PSUBD
(dwordy)PSUBSB
(bajty) / PSUBSW
(słowa).PSUBUSB
(bajty) / PSUBUSW
(słowa)PMULHRWC
, PMULHRIW
, PMULHRWA
- mnożenie
spakowanych słów, zaokrąglanie, zapisanie tylko starszych 16 bitów wyniku (z 32).PMULHUW
- mnożenie spakowanych słów bez znaku, zachowanie starszych 16 bitówPMULHW
, PMULLW
- mnożenie spakowanych słów bez znaku,
zapisanie starszych/młodszych 16 bitów (odpowiednio).PMULUDQ
- mnożenie spakowanych dwordów bez znakuPMADDWD
- do młodszego dworda rejestru docelowego idzie suma
iloczynów 2 najmłodszych słów ze sobą i 2 starszych (bity 16-31) słów ze sobą.
Do starszego dworda - suma iloczynów 2 słów 32-47 i 2 słów 48-63.PCMPEQB
/ PCMPEQW
/ PCMPEQD
(EQ oznacza równość)większe niż:
PCMPGTPB
/ PCMPGTPW
/ PCMPGTPD
(GT oznacza greater than, czyli większy)PACKSSWB
/ PACKSSDW
, PACKUSWB
-
upychająsłowa/dwordy do bajtów/słów i pozostawiają w rejestrze docelowym.
PUNPCKHBW
,
PUNPCKHWD
, PUNPCKHDQ
-
pobierają starsze części bajtów/słów/dwordów z jednego i drugiego rejestru, mieszają
je i zostawiają w pierwszym.PUNPCKLBW
,
PUNPCKLWD
, PUNPCKLDQ
- jak
wyżej, tylko pobierane są młodsze częściPAND
(bitowe AND)PANDN
(najpierw bitowe NOT pierwszego rejestru, potem jego bitowe AND z drugim rejestrem)POR
(bitowe OR)PXOR
(bitowe XOR)SHL
, SHR
i
SAR
, odpowiednio):PSLLW
(słowa) / PSLLD
(dword-y), PSLLQ
(qword)PSRLW
(słowa) / PSRLD
(dword-y), PSRLQ
(qword)PSRAW
(słowa)/ PSRAD
(dword-y)EMMS
- Empty MMX State - ustawia rejestry FPU
jako wolne, umożliwiając ich
użycie. Ta instrukcja musi być wykonana za każdym razem, gdy kończymy pracę z
MMX i chcemy zacząć pracę z FPU.
Rzadko która z tych instrukcji traktuje rejestr jako całość, częściej operują one na poszczególnych wartościach osobno, równolegle.
Spróbuję teraz podać kilka przykładów zastosowania MMX. Ze względu na to, że TASM bez pomocy zewnętrznych plików z makrami nie obsługuje MMX, będę używał składni NASMa i FASMa.
Przykład 1. Dodawanie dwóch tablic bajtów w pamięci. Bez MMX mogłoby to wyglądać mniej-więcej tak:
; EDX - adres pierwszej tablicy bajtów ; ESI - adres drugiej tablicy bajtów ; EDI - adres docelowej tablicy bajtów ; ECX - liczba bajtów w tablicach. Przyjmiemy, że różna od zera... petla: mov al, [edx] ; pobierz bajt z pierwszej add al, [esi] ; dodaj bajt z drugiej mov [edi], al ; zapisz bajt w docelowej inc edx ; zwiększ o 1 indeksy do tablic inc esi inc edi loop petla ; działaj, dopóki ECX różne od 0.
mov ebx, ecx ; EBX = liczba bajtów and ebx, 7 ; będziemy brać po 8 bajtów - obliczamy ; więc resztę z dzielenia przez 8 shr ecx, 3 ; dzielimy ECX przez 8 petla: movq mm0, [edx] ; pobierz 8 bajtów z pierwszej tablicy paddb mm0, [esi]; dodaj 8 spakowanych bajtów z drugiej movq [edi], mm0 ; zapisz 8 bajtów w tablicy docelowej add edx, 8 ; zwiększ indeksy do tablic o 8 add esi, 8 add edi, 8 loop petla ; działaj, dopóki ECX różne od 0. test ebx, ebx ; czy EBX = 0? jz koniec ; jeśli tak, to już skończyliśmy mov ecx, ebx ; ECX = resztka, co najwyżej 7 bajtów. ; te kopiujemy tradycyjnie petla2: mov al, [edx] ; pobierz bajt z pierwszej add al, [esi] ; dodaj bajt z drugiej mov [edi], al ; zapisz bajt w docelowej inc edx ; zwiększ o 1 indeksy do tablic inc esi inc edi loop petla2 ; działaj, dopóki ECX różne od 0 koniec: emms ; wyczyść rejestry MMX, by FPU mogło z nich korzystać
Podobnie będą przebiegać operacje
PAND
, POR
, PXOR
, PANDN
.
Przy dużych ilościach danych, sposób drugi będzie wykonywał około 8 razy mniej instrukcji niż pierwszych, bo dodaje na raz 8 bajtów. I o to właśnie chodziło.
Przykład 2. Kopiowanie pamięci.
Bez MMX:
; DS:SI - źródło ; ES:DI - cel ; ECX - liczba bajtów mov ebx, ecx ; EBX = liczba bajtów and ebx, 3 ; EBX = reszta z dzielenia liczby bajtów przez 4 shr ecx, 2 ; ECX = liczba bajtów dzielona przez 4 cld ; kierunek: do przodu rep movsd ; dword z DS:SI idzie pod ES:DI, DI:=DI+4, ; SI:=SI+4, dopóki CX jest różny od 0 mov ecx, ebx ; ECX = liczba pozostałych bajtów rep movsb ; resztkę kopiujemy po bajcie
mov ebx, ecx ; EBX = liczba bajtów and ebx, 7 ; EBX = reszta z dzielenia liczby bajtów ; przez 8 shr ecx, 3 ; ECX = liczba bajtów dzielona przez 8 petla: movq mm0, [esi] ; MM0 = 8 bajtów z tablicy pierwszej movq [edi], mm0 ; kopiujemy zawartość MM0 pod [EDI] add esi, 8 ; zwiększamy indeksy tablic o 8 add edi, 8 loop petla ; działaj, dopóki ECX różne od 0 mov ecx, ebx ; ECX = liczba pozostałych bajtów cld ; kierunek: do przodu rep movsb ; resztkę kopiujemy po bajcie emms ; wyczyść rejestry MMX
lub, dla solidniejszych porcji danych:
mov ebx, ecx ; EBX = liczba bajtów and ebx, 63 ; EBX = reszta z dzielenia liczby bajtów ; przez 64 shr ecx, 6 ; ECX = liczba bajtów dzielona przez 64 petla: ; kopiuj 64 bajty spod [ESI] do rejestrów MM0, ... MM7 movq mm0, [esi] movq mm1, [esi+8] movq mm2, [esi+16] movq mm3, [esi+24] movq mm4, [esi+32] movq mm5, [esi+40] movq mm6, [esi+48] movq mm7, [esi+56] ; kopiuj 64 bajty z rejestrów MM0, ... MM7 do [EDI] movq [edi ], mm0 movq [edi+8 ], mm1 movq [edi+16], mm2 movq [edi+24], mm3 movq [edi+32], mm4 movq [edi+40], mm5 movq [edi+48], mm6 movq [edi+56], mm7 add esi, 64 ; zwiększ indeksy do tablic o 64 add edi, 64 loop petla ; działaj, dopóki ECX różne od 0 mov ecx, ebx ; ECX = liczba pozostałych bajtów cld ; kierunek: do przodu rep movsb ; resztkę kopiujemy po bajcie emms ; wyczyść rejestry MMX
Przykład 3. Rozmnożenie
jednego bajtu na cały rejestr MMX.
org 100h movq mm0, qword [wart1] ; mm0 = 00 00 00 00 00 00 00 33 ; (33h = kod ASCII cyfry 3) punpcklbw mm0, mm0 ; do najmłodszego słowa włóż najmłodszy bajt ; mm0 i najmłodszy bajt mm0 (czyli ten sam) ; mm0 = 00 00 00 00 00 00 33 33 punpcklwd mm0, mm0 ; do najmłodszego dworda włóż dwa razy ; najmłodsze słowo mm0 ; mm0 = 00 00 00 00 33 33 33 33 punpckldq mm0, mm0 ; do najmłodszego (i jedynego) qworda włóż 2x ; najmłodszy dword mm0 obok siebie ; mm0 = 33 33 33 33 33 33 33 33 movq [wart2], mm0 emms ; wyczyść rejestry MMX mov dx, wart2 mov ah, 9 int 21h ; wypisz ciąg znaków wart2 zakończony znakiem dolara mov ax, 4c00h int 21h wart1 db "3" ; cyfra 3 times 7 db 0 ; i 7 bajtów zerowych wart2: times 8 db 2 ; 8 bajtów o wartości 2 db "$" ; dla int 21h/ah=9
Kompilujemy, uruchamiamy i ... rzeczywiście na ekranie pojawia się upragnione osiem trójek!
Technologia MMX może być używana w wielu celach, ale jej najbardziej korzystną cechą jest właśnie równoległość wykonywanych czynności, dzięki czemu można oszczędzić czas procesora.
Krótko mówiąc, SSE jest dla MMX tym, czym FPU jest dla CPU. To znaczy, SSE przeprowadza
równoległe operacje na liczbach ułamkowych.
SSE operuje już na całkowicie osobnych rejestrach nazwanych xmm0, ..., xmm7 po 128 bitów każdy.
W trybie 64-bitowym dostępne jest dodatkowych 8 rejestrów: xmm8, ..., xmm15.
Prawie każda operacja związana z danymi w pamięci musi mieć te dane ustawione na 16-bajtowej
granicy, czyli jej adres musi się dzielić przez 16. Inaczej generowane jest przerwanie (wyjątek).
SSE 2 różni się od SSE kilkoma nowymi instrukcjami konwersji ułamek-liczba całkowita oraz tym, że może operować na liczbach ułamkowych rozszerzonej precyzji (64 bity).
U AMD częściowo 3DNow! operuje na ułamkach, ale co najwyżej na dwóch gdyż są to rejestry odpowiadające MMX, a więc 64-bitowe. 3DNow! Pro jest odpowiednikiem SSE w procesorach AMD. Odpowiedniki SSE2 i SSE3 pojawiły się w AMD64.
Instrukcje SSE (nie wszystkie będą wymienione):
MOVAPS
- move aligned packed single precision floating point values
- przemieść ułożone
(na granicy 16 bajtów) spakowane ułamki pojedynczej precyzji (4 sztuki po 32 bity)MOVUPS
- move unaligned (nieułożone) packed single
precision floating point valuesMOVSS
- move scalar (1 sztuka, najmłodsze 32 bity rejestru)
single precision floating point valueADDPS
- add packed single precision floating point values =
dodawanie czterech ułamków do czterechADDSS
- add scalar single precision floating point values =
dodawanie jednego ułamka do innegoMULPS
- mnożenie spakowanych ułamków, równolegle, 4 paryMULSS
- mnożenie jednego ułamka przez innyDIVPS
- dzielenie spakowanych ułamków, równolegle, 4 paryDIVSS
- dzielenie jednego ułamka przez innyANDPS
- logiczne AND spakowanych wartości (ale oczywiście tym bardziej zadziała
dla jednego ułamka w rejestrze)ANDNPS
- AND NOT (najpierw bitowe NOT pierwszego rejestru, potem jego bitowe AND z
drugim rejestrem) dla spakowanychORPS
- OR dla spakowanychXORPS
- XOR dla spakowanychCMPPS
, CMPSS
, (U)COMISS
W większości przypadków instrukcje dodane w SSE 2 różnią się od powyższych ostatnią literą, którą
jest D, co oznacza double precision
, na przykład MOVAPD
.
No i krótki przykładzik. Inna wersja procedury do kopiowania pamięci. Tym razem z SSE.
; Tylko jeśli ESI i EDI dzieli się przez 16! Inaczej używać MOVUPS. mov ebx, ecx ; EBX = liczba bajtów and ebx, 127 ; EBX = reszta z dzielenia liczby bajtów ; przez 128 shr ecx, 7 ; ECX = liczba bajtów dzielona przez 128 petla: ; kopiuj 128 bajtów spod [ESI] do rejestrów XMM0, ... XMM7 movaps xmm0, [esi] movaps xmm1, [esi+16] movaps xmm2, [esi+32] movaps xmm3, [esi+48] movaps xmm4, [esi+64] movaps xmm5, [esi+80] movaps xmm6, [esi+96] movaps xmm7, [esi+112] ; kopiuj 128 bajtów z rejestrów XMM0, ... XMM7 do [EDI] movaps [edi ], xmm0 movaps [edi+16 ], xmm1 movaps [edi+32 ], xmm2 movaps [edi+48 ], xmm3 movaps [edi+64 ], xmm4 movaps [edi+80 ], xmm5 movaps [edi+96 ], xmm6 movaps [edi+112], xmm7 add esi, 128 ; zwiększ indeksy do tablic o 128 add edi, 128 loop petla ; działaj, dopóki ECX różne od 0 mov ecx, ebx ; ECX = liczba pozostałych bajtów cld ; kierunek: do przodu rep movsb ; resztkę kopiujemy po bajcie
Nie jest to ideał, przyznaję. Można było na przykład użyć instrukcji
wspierających pobieranie danych z pamięci: PREFETCH
.
A teraz coś innego: rozdzielanie danych. Przypuśćmy, że z jakiegoś urządzenia (lub pliku) czytamy bajty w postaci XYXYXYXYXY..., a my chcemy je rozdzielić na dwie tablice, zawierające tylko XXX... i YYY... (oczywiście bajty mogą mieć różne wartości, ale idea jest taka, że co drugi chcemy mieć w drugiej tablicy). Oto, jak można tego dokonać z użyciem SSE2 (składnia NASM/FASM, bo TASM w ogóle nie zna SSE). To jest tylko fragment programu.
mov ah, 9 mov dx, dane_pocz int 21h mov ah, 9 mov dx, dane int 21h ; wypisz dane początkowe ; FASM: "movaps xmm0, dqword [dane]" movaps xmm0, [dane] movaps xmm1, xmm0 ; XMM1=XMM0 = X1Y1 X2Y2 X3Y3 X4Y4 X5Y5 X6Y6 X7Y7 X8Y8 ; XXM* muszą zawierać tylko po jednym bajcie w każdym słowie psllw xmm0, 8 ; teraz XMM0 = Y1 0 Y2 0 Y3 0 Y4 0 Y5 0 Y6 0 Y7 0 Y8 0 psrlw xmm0, 8 ; teraz XMM0 = 0 Y1 0 Y2 0 Y3 0 Y4 0 Y5 0 Y6 0 Y7 0 Y8 psrlw xmm1, 8 ; teraz XMM1 = 0 X1 0 X2 0 X3 0 X4 0 X5 0 X6 0 X7 0 X8 packuswb xmm0, xmm0 ; teraz XMM0 = Y1Y2 Y3Y4 Y5Y6 Y7Y8 Y1Y2 Y3Y4 Y5Y6 Y7Y8 packuswb xmm1, xmm1 ; teraz XMM1 = X1X2 X3X4 X5X6 X7X8 X1X2 X3X4 X5X6 X7X8 ; FASM: "movq qword [dane2], xmm0" movq [dane2], xmm0 ; dane2 ani dane1 już nie mają adresu ; podzielnego przez 16, więc nie można ; użyć MOVAPS a my i tak ; chcemy tylko 8 bajtów ; FASM: "movq qword [dane1], xmm1" movq [dane1], xmm1 mov ah, 9 mov dx, dane_kon int 21h mov ah, 9 mov dx, dane1 int 21h ; wypisz pierwsze dane końcowe mov ah, 9 mov dx, dane2 int 21h ; wypisz drugie dane końcowe mov ax, 4c00h int 21h align 16 ; dla SSE dane db "ABCDEFGHIJKLMNOP", 10, 13, "$" dane1 db 0, 0, 0, 0, 0, 0, 0, 0, 13, 10, 9, "$" dane2 db 0, 0, 0, 0, 0, 0, 0, 0, 13, 10, "$" dane_pocz db "Program demonstrujacy SSE. Dane na poczatku: ", 10, 13, 9, "$" dane_kon db "Dane na koncu: ", 10, 13, 9, "$"
Po szczegółowy opis wszystkich instrukcji odsyłam, jak zwykle do Intela i AMD.
Instrukcje typu SIMD wspomagają szybkie przetwarzanie multimediów: dźwięku, obrazu. Omówienie każdej instrukcji w detalu jest niemożliwe i niepotrzebne, gdyż szczegółowe opisy są zamieszczone w książkach Intela lub AMD.
Miłej zabawy.