Autor: | Wojciech Muła |
---|---|
Dodany: | 31.01.2002 |
Aktualizacja: | 5.07.2007 |
Treść
Główni konkurenci Intela, czyli AMD i Cyrix, podczas projektowania swoich implementacji MMX ruszyli głową i wprowadzili kilka rozszerzeń w stosunku do oryginalnego zestawu instrukcji. Okazały się na tyle interesujące i przydatne, że Intel wprowadził je do zestawu instrukcji wraz z SSE.
Artykuł powstał, by pokazać co stracili posiadacze starych procesorów Pentium MMX.
Składnia:
PMAXUB mmreg1, mmreg2/mem64 PMINUB mmreg1, mmreg2/mem64
Działanie (pokazane dla PMAXUB):
mmreg1[ 0.. 7] = (mmreg1[ 0.. 7] > mmreg2/mem64[ 0.. 7]) ? mmreg1[ 0.. 7] : mmreg2/mem64[ 0.. 7] mmreg1[ 8..15] = (mmreg1[ 8..15] > mmreg2/mem64[ 8..15]) ? mmreg1[ 8..15] : mmreg2/mem64[ 8..15] ... ; itd. mmreg1[56..63] = (mmreg1[56..63] > mmreg2/mem64[56..63]) ? mmreg1[56..63] : mmreg2/mem64[56..63]
Implementacja PMAXUB:
; mmreg1 = mm0 ; np. mm0 = |10|50|60|80|... ; mmreg2 = mm1 ; mm1 = | 5|80|60|90|... ; niszczy: mm2, mm3 pxor mm3, mm3 ; mm3 = packed_byte(0x00) movq mm2, mm1 ; *** psubusb mm2, mm0 ; mm2 = | 0|30| 0|10|... pcmpeq mm2, mm3 ; mm2 = |ff|00|ff|00|... pand mm0, mm2 ; mm0 = |10| 0|60| 0|... pandn mm2, mm1 ; mm2 = | 0|80| 0|90|... por mm0, mm2 ; mm0 = |10|80|60|90|...
Implementacja PMINUB jest w zasadzie taka sama, wystarczy linijkę oznaczoną *** zamienić na movq mm2, mm0.
W analogiczny sposób można operować na słowach i dwusłowach bez znaku.
Czytając ostatnio Assembly Programming Journal #5 zwróciłem uwagę na kod Chrisa Dragana wyznaczania minimum i maksimum liczb bez znaku. Oto zastosowany algorytm:
int max(int a, int b) { int diff_max; if ((diff_max=b-a) < 0) // odejmowanie z nasyceniem diff_max = 0; return a + diff_max; }
(W podobny sposób można obliczyć minimum).
Odejmowanie z nasyceniem jest realizowane przez MMX w jednej instrukcji, wobec czego kod rozkazu PMAXUB przedstawia się następująco:
; a = mm0 = |10|50|60|80|... ; b = mm1 = | 5|80|60|90|... psubusb mm1, mm0 ; mm1 = | 0|30| 0|10| paddb mm0, mm1 ; mm0 = |10|30|60|90|
Składnia:
PMAXSW mmreg1, mmreg2/mem64 PMINSW mmreg1, mmreg2/mem64
Działanie (operacje na packed_word):
; min mmreg1 = (mmreg1 < mmreg2/mem64) ? mmreg1 : mmreg2/mem64 ; max mmreg1 = (mmreg1 > mmreg2/mem64) ? mmreg1 : mmreg2/mem64
Implementacja PMAXSW:
; mmreg1 = mm0 ; mmreg2 = mm1 movq mm2, mm0 pcmpgtw mm2, mm1 ; mm2 = (mm0 > mm1) ? 0xffff : 0x0000 *** pand mm0, mm2 ; mm0 = (mm0 > mm1) ? mm0 : 0x0000 pandn mm2, mm1 ; mm0 = (mm1 > mm0) ? mm1 : 0x0000 por mm0, mm1
Wystarczy zamienić rozkaz w linijce oznaczonej *** na pcmpgtw mm2, mm0, by powyższy kod symulował działanie rozkazu PMINSW.
Składnia:
pavgusb mmreg1, mmereg2/mem
Opis (operacje na packed_byte):
mmreg1 = (mmreg1 + mmreg2 + 1) >> 1
Implementacja 1:
Ponieważ średnia jest liczona z trzech, a nie dwóch argumentów, najrozsądniej jest rozszerzyć zakresy liczb. Unika się wtedy wielu problemów z wykrywaniem przekroczenia zakresu.
; mmereg1 = mm0 ; mmereg2 = mm1 pxor mm4, mm4 ; mm5 = packed_word(0x0000) movq mm2, mm0 movq mm3, mm1 punpcklbw mm0, mm4 ; punpckhbw mm2, mm4 ; mm2:mm0 = extend_bw(mm0) punpcklbw mm1, mm4 ; punpckhbw mm3, mm4 ; mm3:mm1 = extend_bw(mm1) pcmpeqw mm5, mm5 ; mm5 = packed_word(0xffff) psubw mm4, mm5 ; mm3 = packed_word(0x0001) paddw mm0, mm1 ; mm2:mm0 += mm3:mm1 paddw mm2, mm3 ; paddw mm0, mm4 ; mm2:mm0 += 1 paddw mm2, mm4 ; psrlw mm0, 1 ; /2 psrlw mm2, 1 ; /2 packuswb mm0, mm2 ; mm0 = packed_uw2ub(mm2:mm0)
Implementacja 2 [5.07.2007]:
Lepszy sposób realizacji instrukcji pavgb (czy pavgusb), który równolegle wykonuje działanie na bajtach bez znaku:
mm1[i] = (mm1[i] + mm2[i] + 1) >> 1, i=0..7
Metodę pokażę na przykładzie dla jednego bajtu:
-- a, b - argumenty (bajty) a1 = (a & 0xfe) >> 1 -- a1, b1 - starsze 7 bitów b1 = (b & 0xfe) >> 1 cc = (a | b) & 0x01 -- bit przeniesienia z pozycji 0 -- dla działania a + b + 1 avg = a1 + a2 + cc -- średnia (a + b + 1) >> 1
Jedna bardzo miła cecha tej metody, to także możliwość zastosowania jej w zwykłym, nie-MMX-owym kodzie. Np.:
; eax, ebx - wektory 4 bajtów movl %eax, %ecx orl %ebx, %ecx andl $0x01010101, %ecx ; ecx = cc shrl $1, %eax andl $0x7f7f7f7f, %eax ; eax = a1 shrl $1, %ebx andl $0x7f7f7f7f, %ebx ; ebx = b1 addl %ebx, %eax addl %ecx, %eax ; eax = avg
Z MMX-em będzie tylko taka różnica, że trzeba maski bitowe albo mieć załadowane do jakiś rejestrów, albo odczytywać z pamięci.
Składnia:
PEXTRW reg32, mmreg, imm8
Opis:
index = imm[1:0]*16 reg32[31:16] = 0 reg32[15: 0] = mmreg[index+15:index]
Implementacja:
; reg32 = eax ; mmreg = mm0 ; imm8 = ? index equ (imm8 & 0x3)*16 psrlq mm0, index ; umieść wymagane słowo na pozycji 0 movd eax, mm0 ; przepisz do rejestru and eax, 0x0000ffff ; i wyzeruj starsze słowo
Składnia:
PINSRW mmreg, reg32, imm8
Opis:
index = imm8[1:0]*16 mmreg[index+15:index] = reg32[15:0]
Implementacja:
; mmreg = mm0 ; reg32 = eax ; imm8 = ? index equ (imm8 & 0x3)*16 ; np. index=2*16=32 movq mm1, mm0 pcmpeqw mm0, mm0 ; mm1 = |ffff|ffff|ffff|ffff| psrlq mm0, 3*16 ; mm1 = |0000|0000|0000|ffff| movd mm2, eax ; mm2 = |0000|0000|xxxx|aaaa| pand mm2, mm0 ; mm2 = |0000|0000|0000|aaaa| psllq mm2, index ; mm2 = |0000|aaaa|0000|0000| psll1 mm0, index ; mm0 = |0000|ffff|0000|0000| pandn mm0, mm1 ; mm0 = (not mm0) and mm2 por mm0, mm2
Jeśli maska |0000|0000|0000|ffff| jest w pamięci i dodatkowo założymy, że starsze słowo reg32 jest wyzerowane, to kod będzie krótszy.
movq mm1, mm0 movq mm0, [mmx_mask] movd mm2, eax ; mm2 = |0000|0000|xxxx|aaaa| psllq mm2, index ; mm2 = |0000|aaaa|0000|0000| psll1 mm0, index ; mm0 = |0000|ffff|0000|0000| pandn mm0, mm1 ; mm0 = (not mm0) and mm2 por mm0, mm2
Składnia:
PMOVMSKB reg32, mmreg
Opis: z najstarszych bitów każdego bajtu tworzy jeden bajt
reg32[31:8] = 0 reg32[7] = mmreg[63] reg32[6] = mmreg[55] reg32[5] = mmreg[47] reg32[4] = mmreg[39] reg32[3] = mmreg[31] reg32[2] = mmreg[23] reg32[1] = mmreg[15] reg32[0] = mmreg[7]
Implementacja #1:
; mmreg = mm0 ; reg32 = eax ; mask = packed_byte(0x01) ; mm0 = axxxxxxx bxxxxxxx cxxxxxxx dxxxxxxx exxxxxxx fxxxxxxx gxxxxxxx hxxxxxxx psrlq mm0, 7 ; pand mm0, [maxsk] ; mm0 = _______a _______b _______c _______d _______e _______f _______g _______h ; A = (A >> 7) | A movq mm1, mm0 ; mm1 = _______a _______b _______c _______d _______e _______f _______g _______h psrlq mm0, 7 ; mm0 = ________ ______a_ ______b_ ______c_ ______d_ ______e_ ______f_ ______g_ por mm0, mm1 ; mm0 = _______a ______ab ______bc ______cd ______de ______ef ______fg ______gh ; A = (A >> 14) | A movq mm1, mm0 ; mm1 = _______a ______ab ______bc ______cd ______de ______ef ______fg ______gh psrlq mm0, 14 ; mm0 = ________ ________ _____a__ ____ab__ ____bc__ ____cd__ ____de__ ____ef__ por mm0, mm1 ; mm0 = _______a ______ab _____abc ____abcd ____bcde ____cdef ____defg ____efgh ; A = (A >> 28) | A movq mm1, mm0 ; mm1 = _______a ______ab _____abc ____abcd ____bcde ____cdef ____defg ____efgh psrlq mm0, 28 ; mm0 = ________ ________ ________ ________ ___a____ __ab____ _abc____ abcd____ por mm0, mm1 ; mm0 = _______a ______ab _____abc ____abcd ___abcde __abcdef _abcdefg **abcdefgh** movd eax, mm0 and eax, 0x000000ff
Implementacja #2:
segment .text psrlq mm0, 7 ; przesuń MSB na pozycję 0 pand mm0, [mask] ; pozostaw tylko bity 0 ; mm0 = | _______a _______b | _______c _______d | _______e _______f | _______g _______h | pmaddwd mm0, [const] ; starsze dwusłowo: ; A = | ________ ________ | _______b _______a | * ((1 << 1) + (1 << 8)) = | ________ _______b | ______ba ______a_ | ; B = | ________ ________ | _______d _______c | * ((1 << 3) + (1 << 10)) = | ________ _____d__ | ____dc__ ____c___ | ; ; A+B = | ________ _____d_b | ____**dcba** ____c_a_ | ; ; młodsze słowo analogicznie movq mm1, mm0 ; mm1 = | ________ _____d_b | ____dcba ____c_a_ | ________ _____h_f | ____hgfe ____g_e_ | psrlq mm0, 4 ; mm0 = | ________ ________ | _____d_b ____dcba | ____c_a_ ________ | _h_f____ hgfe____ | psrlq mm1, 40 ; mm1 = | ________ ________ | ________ ________ | ________ ________ | _____d_b ____dcba | por mm0, mm1 ; mm0 = | ________ ________ | ________ ________ | ________ ________ | _h_f_d_b hgfedcba | movd eax, mm0 ; eax = | _h_f_d_b hgfedcba | and eax, 0x000000ff ; eax = | ________ hgfedcba | segment .data const dw ((1 << 1) + (1 << 8)) dw ((1 << 3) + (1 << 10)) dw ((1 << 1) + (1 << 8)) dw ((1 << 3) + (1 << 10)) mask dd 0101010h, 0101010h
Składnia:
MASKMOVQ mmreg1, mmreg2 (edi)
Opis: przypisanie warunkowe bajtów
memory[edi][63:56] = mmreg2[63] ? mmreg1[63:56] : memory[edi][63:56] memory[edi][55:48] = mmreg2[55] ? mmreg1[55:48] : memory[edi][55:48] memory[edi][47:40] = mmreg2[47] ? mmreg1[47:40] : memory[edi][47:40] memory[edi][39:32] = mmreg2[39] ? mmreg1[39:32] : memory[edi][39:32] memory[edi][31:24] = mmreg2[31] ? mmreg1[31:24] : memory[edi][31:24] memory[edi][23:16] = mmreg2[23] ? mmreg1[23:16] : memory[edi][23:16] memory[edi][15: 8] = mmreg2[15] ? mmreg1[15: 8] : memory[edi][15: 8] memory[edi][ 7: 0] = mmreg2[7] ? mmreg1[ 7: 0] : memory[edi][ 7: 0]
Implementacja:
Jedyny istotny problem to wygenerowanie maski.
; mmreg1 = mm0 ; mmreg2 = mm1 ; wygenerowanie w mm3 packed_byte(0x80) pcmpeqw mm3, mm3 psllw mm3, 8 packsswb mm3, mm3 ; generacja maski dla mmreg1 pand mm1, mm3 pcmpeqb mm1, mm3 ; wykonanie warunku pand mm0, mm3 pandn mm3, [edi] por mm0, mm3 ; zapis do pamięci movq [edi], mm0
Składnia:
PSADBW mmreg1, mmreg2
Opis:
mmreg1[63:16] = 0 mmreg1[15: 0] = abs(mmreg1[ 7: 0] - mmreg2[ 7: 0]) + abs(mmreg1[15: 8] - mmreg2[15: 8]) + abs(mmreg1[23:16] - mmreg2[23:16]) + abs(mmreg1[31:24] - mmreg2[31:24]) + abs(mmreg1[39:32] - mmreg2[39:32]) + abs(mmreg1[47:40] - mmreg2[47:40]) + abs(mmreg1[55:48] - mmreg2[55:48]) + abs(mmreg1[63:56] - mmreg2[63:56])
Implementacja:
Pierwszym krokiem jest policzenie modułów różnic dla każdego bajtu, następnie należy do siebie dodać każdy bajt (co jest chyba najbardziej czasochłonne).
; mmreg1 = mm0 ; mmreg1 = mm1 ; obliczenie modułu różnic (za Intel MMX Developers Guide) movq mm2, mm0 ; psubusb mm0, mm1 ; tam gdzie różnica jest mniejsza od zera wynik jest zerem psubusb mm1, mm2 ; por mm0, mm1 ; połączenie wyników: mm0 = abs(mm0-mm1) ; mm0 = |h|g|f|e|d|c|b|a| ; rozszerzania zakresu liczb bez znaku z bajtu do słowa movq mm1, mm0 punpcklbw mm0, mm0 ; mm0 = |d|d|c|c|b|b|a|a| psrlw mm0, 8 ; mm0 = |0|d|0|c|0|b|0|a| = |d|c|b|a| punpckhbw mm1, mm1 ; mm1 = |h|h|g|g|f|f|e|e| psrlw mm1, 8 ; mm1 = |0|h|0|g|0|f|0|e| = |h|g|f|e| ; sumowanie ; mm0 = | d | c | b | a | paddw mm0, mm1 ; mm0 = | dh | cg | bf | ae | movq mm1, mm0 psllq mm1, 16 ; mm1 = | cg | bf | ae | 0 | paddw mm0, mm1 ; mm0 = | cdgh | bcfg | abef | ae | movq mm1, mm0 psllq mm1, 32 ; mm1 = | abef | ae | 0 | 0 | paddw mm0, mm1 ; mm0 = |**abcdefgh**| ... reszta nieważna .... | psrlq mm0, 48 ; przesuń najstarsze słowo na pozycję zero movd eax, mm0
Składnia:
PSHUFW mmreg1, mmreg2, imm8
Opis: rozmieszczenie słów zgodnie z podanym schematem
index3 = imm8[7:6] * 16 index2 = imm8[5:4] * 16 index1 = imm8[3:2] * 16 index0 = imm8[1:0] * 16 temp = mmreg2 mmreg1[63:48] = temp[index3+15:index3] mmreg1[47:32] = temp[index2+15:index2] mmreg1[31:16] = temp[index1+15:index1] mmreg1[15:0] = temp[index0+15:index0]
Przykład:
; mm0 = |d|c|b|a| pshuf mm1, mm0, 0xe3 ; 0xe3 = 11 10 00 11b ; nr0 = 3 ; nr1 = 0 ; nr2 = 2 ; nr3 = 3 ; mm1 = |d|c|a|d|
Implementacja:
; mmreg1 = mm1 ; mmreg2 = mm0 index0 equ ((imm8 >> 0 & 3*16 ; 16 index1 equ ((imm8 >> 2 & 3*16 ; 32 index2 equ ((imm8 >> 4 & 3*16 ; 48 index3 equ ((imm8 >> 6 & 3*16 ; 0 ; mm0 = d c b a pxor mm1, mm1 ; mm1 = 0 0 0 0 pcmpeqb mm2, mm2 ; mm2 = f f f f psllq mm2, 48 ; mm2 = f 0 0 0 movq mm3, mm0 ; mm3 = d c b a psllq mm3, 48-index0 ; mm3 = b a 0 0 pand mm3, mm2 ; mm3 = b 0 0 0 psrlq mm3, 48 ; mm3 = 0 0 0 b por mm1, mm3 ; mm1 = 0 0 0 b movq mm3, mm0 ; mm3 = d c b a psllq mm3, 48-index1 ; mm3 = c 0 0 0 pand mm3, mm2 ; mm3 = c 0 0 0 psrlq mm3, 32 ; mm3 = 0 0 c 0 por mm1, mm3 ; mm1 = 0 0 c b movq mm3, mm0 ; mm3 = d c b a psllq mm3, 48-index2 ; mm3 = d c b a pand mm3, mm2 ; mm3 = d 0 0 0 psrlq mm3, 16 ; mm3 = 0 d 0 0 por mm1, mm3 ; mm1 = 0 d c b movq mm3, mm0 ; mm3 = d c b a psllq mm3, 48-index3 ; mm3 = a 0 0 0 pand mm3, mm2 ; mm3 = a 0 0 0 por mm1, mm3 ; mm1 = a d a b
Rozkaz ten jest podobny do PMULHW, ale operuje na liczbach bez znaku. Więc dla argumentów z przedziału 0x0000 do 0x7fff wyniki zwracane przez oba rozkazy będą takie same, dla pozostały będą się różnić. Ta cecha zostanie wykorzystana; otóż słowa A i B można zapisać jako:
a = A & 0x7fff + A & 0x8000 = lo(A) + msb(A) << 15 b = B & 0x7fff + B & 0x8000 = lo(B) + msb(B) << 15
wtedy mnożenie ma postać:
a*b = lo(A)*lo(B) + lo(A)*(msb(B) << 15) + lo(B)*(msb(A) << 15) + (msb(A) & msb(B)) << 30
Rozkład znaczących bitów (x) dla poszczególnych czynników jest następujący:
starsze słowo | młodsze słowo ---- ---- ---- ----|---- ---- ---- ---- 0000 xxxx xxxx xxxx|**x**xxx xxxx xxxx xxxx lo(A)*lo(B) 0xxx xxxx xxxx xxxx|**x**000 0000 0000 0000 lo(A)*(msb(B) << 15) 0xxx xxxx xxxx xxxx|**x**000 0000 0000 0000 lo(B)*(msb(A) << 15) 00x0 0000 0000 0000|0000 0000 0000 0000 (msb(A) & msb(B)) << 30
O tym czy młodsze słowo wpływa na starsze decydują pogrubione bity: tak więc faktycznie potrzebne jest najstarszy bit wyniku lo(A)*lo(B), oraz najmłodsze lo(A) i lo(B). Wyrażenie wyznaczające starsze słowo można zapisać w sposób następujący:
result = lo(A)*lo(B) >> 30 # wyznaczone przez pmulhw tmp = lo(A)*lo(B) & 0xffff # wyznaczone przez pmullw tmp = tmp >> 15 if msb(B): result += lo(A) >> 1 tmp += lo(A) >> 15 if msb(A): result += lo(B) >> 1 tmp += lo(B) >> 15 if msb(A) & msb(B): result += 1 << 14 # 0x4000 result += tmp >> 1
Składnia:
pmuluhw mmreg1, mmreg2/mem64
Opis: (rozkaz działa na packed_word)
mmreg1 = ((unsigned)mmreg1 * (unsigned)mmreg2/mem64) >> 16
Implementacja:
; mm0 - mmreg1 (A) ; mm1 - mmreg2 (B) movq mm2, mm0 movq mm3, mm1 pand mm0, [pw_0x7ffff] ; mm0 = lo(A) pand mm1, [pw_0x7ffff] ; mm1 = lo(B) pand mm2, [pw_0x80000] ; mm2 = (msb(A) << 15) pand mm3, [pw_0x80000] ; mm3 = (msb(B) << 15) ; result = lo(A)*lo(B) >> 30 # wyznaczone przez pmulhw ; tmp = lo(A)*lo(B) & 0xffff # wyznaczone przez pmullw movq mm7, mm0 ; zachowaj lo(A) movq mm6, mm1 ; i lo(B) pmulhw mm0, mm1 ; mm0 = result pmullw mm1, mm7 ; mm1 = tmp >> 15 psrlw mm1, 15 ;if msb(B): ; result += lo(A) >> 1 ; tmp += lo(A) >> 15 pcmpeqw mm3, [pw_0x8000] ; mm3[i] = msb(B[i]) ? 0xffff : 0x0000 pand mm7, mm3 ; psrlw mm7, 1 ; result += lo(A) >> 1 paddw mm0, mm7 psrlw mm7, 14 ; tmp += lo(A) >> 15 paddw mm1, mm7 ;if msb(A): ; result += lo(B) >> 1 ; tmp += lo(B) >> 15 pcmpeqw mm2, [pw_0x8000] ; mm2[i] = msb(A[i]) ? 0xffff : 0x0000 pand mm6, mm2 ; psrlw mm6, 1 ; result += lo(B) >> 1 paddw mm0, mm6 psrlw mm6, 14 ; tmp += lo(B) >> 15 paddw mm1, mm6 ;if msb(A) & msb(B): ; result += 1 << 14 # 0x4000 pand mm2, mm3 pand mm2, [pw_0x4000] paddw mm0, mm2 ;result += tmp >> 1 psrlw mm1, 1 paddw mm0, mm1
Zaprezentowane rozkazy w procesorach Cyrix używają tzw. implied registers — numer rejestru nie jest żadnym z podanych, lecz jest w jakiś sposób obliczany (nie mam dokumentacji, więc nie potrafię nic więcej powiedzieć).
Składnia:
PDISTUB mmreg1, mmreg2
Opis: (rozkaz operuje na packed_byte)
mmreg1 = abs(mmreg1-mmreg2)
Implementacja:
; mmreg1 = mm0 ; mmreg2 = mm1 movq mm2, mm0 psubusb mm2, mm0 ; zostają tylko różnice większe od zera psubusb mm0, mm1 ;
Składnia:
PMAGW mmreg1, mmreg2/mem64
Opis: (rozkaz operuje na packed_word ze znakiem)
mmreg1 = (abs(mmreg1) > abs(mmreg2)) ? mmreg1 : mmreg2
Implementacja:
; mmreg1 = mm0 ; mmreg2 = mm1 movq mm2, mm0 ; movq mm4, mm0 ; psraw mm4, 15 ; pxor mm2, mm4 ; psubw mm2, mm4 ; mm2 = abs(mm0) movq mm3, mm1 ; movq mm4, mm1 ; psraw mm4, 15 ; pxor mm3, mm4 ; psubw mm3, mm4 ; mm3 = abs(mm1) pcmpgtw mm2, mm3 ; mm2 = (abs(mm0) > abs(mm1)) ? 0xffff : 0x0000; pand mm0, mm2 ; zamaskuj słowa w mm0 pandn mm2, mm1 ; i mm1 por mm0, mm2 ; połącz
Dokument utworzony przez rozszerzony rst2html.