Autor: | Wojciech Muła |
---|---|
Dodany: | 9.02.2002 |
Aktualizacja: | 28.10.2002 |
Treść
Słowa wejściowe są dodatnie (w zakresie 0x0000...0x7fff).
; 9-02-2002 ; wejście: mm1 = |0000|dddd|0000|cccc| ; mm0 = |0000|bbbb|0000|aaaa| ; wyjście: mm0 = |dddd|cccc|bbbb|aaaa| packssdw mm0, mm0 ; mm0 = |bbbb|aaaa|bbbb|aaaa| packssdw mm1, mm1 ; mm1 = |dddd|cccc|dddd|cccc| punpckldq mm0, mm1 ;= |dddd|cccc|bbbb|aaaa|
Słowa wejściowe są liczbami ze znakiem.
; 9-02-2002 ; wejście: mm1 = |0000|dddd|0000|cccc| ; mm0 = |0000|bbbb|0000|aaaa| ; wyjście: mm0 = |dddd|cccc|bbbb|aaaa| movq mm2, mm0 ; mm2 = |0000|bbbb|0000|aaaa| psrlq mm2, 32 ; mm2 = |0000|0000|0000|bbbb| punpcklwd mm0, mm2 ; mm0 = |0000|0000|bbbb|aaaa| movq mm2, mm1 ; mm1 = |0000|dddd|0000|cccc| psllq mm2, 32 ; mm2 = |0000|cccc|0000|0000| punpckhwd mm2, mm1 ; mm2 = |0000|0000|dddd|cccc| punpckldq mm0, mm2 ; mm0 = |dddd|cccc|bbbb|aaaa|
Rozkaz psadbw umożliwia operacje na bajtach, przy jego użyciu bardzo łatwo policzyć sumę bajtów. Ale można użyć go do policzenia sumy słów, albo nawet podwójnych słów (to już raczej w SSE2, ze względu na efektywność). Sumę dwóch słów, można zapisać jako:
a + b = lo_byte(a) + lo_byte(b) + 256*(hi_byte(a) + hi_byte(b))
i przy użyciu w/w rozkazu oddzielnie policzyć sumę młodszych oraz starszych bajtów, by dopiero na końcu uwzględnić ich wagi.
segment .text ; wejście: mm0 = | w3 | w2 | w1 | w0 | ; wyjście: mm0 = | ?? | ?? | ?? |w3+w2+w1+w0| pxor mm2, mm2 ; mm2 = packed_byte(0x00) movq mm1, mm0 pand mm0, [lo_byte] ; mm0 - młodsze bajty pand mm1, [hi_byte] ; mm1 - starsze bajty psadbw mm0, mm2 ; suma młodszych bajtów (sl) psadbw mm1, mm2 ; suma starszych bajtów (sh) psrlq mm1, 8 ; sh*256 paddd mm0, mm1 ; sl + sh*256 segment .data lo_byte dw 0x00ff, 0x00ff, 0x00ff, 0x00ff hi_byte dw 0xff00, 0xff00, 0xff00, 0xff00
Rozszerzenie AMD 3DNow! służy obliczeniom równoległym na dwu liczbach floating-point (przechowywanych w standardowych rejestrach MMX). Jeden z rozkazów jest moim zdaniem szczególnie użyteczny pfacc którego działanie jest następujące:
pfadd mmxreg1, mmxreg2/mem64 mmxreg1[ 0..31] = mmxreg1[0..31] + mmxreg1[32..63] mmxreg1[32..63] = mmxreg2[0..31] + mmxreg2[32..63]
Jak się można łatwo domyśleć, Intel nie wprowadził analogicznego rozkazu ani w SSE ani w SSE2.
Iloczyn skalarny wektora czterowymiarowego:
; esi -> v0 (x0, y0, z0, w0) ; edi -> v1 (x1, y1, z1, w1) movq mm0, [esi] ; mm0 = | y0 | x0 | movq mm1, [esi+8] ; mm1 = | w0 | z0 | pfmul mm0, [edi] ; mm0 = | y0*y1 | x0*x1 | pfmul mm1, [edi] ; mm1 = | w0*w1 | z0*z1 | pfacc mm0, mm1 ; mm0 = | z0*z1 + w0*w1 | x0*x1 + y0*y1 | pfacc mm0, mm0 ; mm0 = | v1*v2 | v1*v2 |
Iloczyn skalarny wektora trójwymiarowego:
; esi -> v0 (x0, y0, z0) ; edi -> v1 (x1, y1, z1) movq mm0, [esi] ; mm0 = | y0 | x0 | movq mm1, [edi] ; mm1 = | y1 | x1 | ; przypominam: ; float(+0.0) = 0 00000000 00000000000000000000000b movd mm2, [esi+8] ; mm1 = | 0 | z0 | movd mm3, [edi+8] ; mm2 = | 0 | z1 | pfmul mm0, mm1 ; mm0 = | y0*y1 | x0*x1 | pfmul mm2, mm3 ; mm2 = | 0 | z0*z1 | pfadd mm0, mm2 ; mm0 = | y0*y1 | x0*x1 + z0*z1 | pfacc mm0, mm0 ; mm0 = | v1*v2 | v1*v2 |
Rozkaz pcmpgt porównuje wyłącznie liczby ze znakiem, porównanie liczb bez znaku wymaga nieco zachodu.
; wejście: ; mm0, mm1 - porównywane wektory ; wyjście: ; mm1 ; niszczy: ; mm2, mm3 pcmpgtub: pxor mm2, mm2 ; mm2 = pb(0x00) pcmpeqb mm3, mm3 ; mm3 = pb(0xff) psubusb mm0, mm1 ; wszystkie elementy mm0 mniejsze ; lub równe mm1 są teraz równe zero pcmpeqb mm0, mm2 ; mm0 = (mm0 <= mm1) ? 0xff : 0x00 pxor mm0, mm3 ; mm0 = ~mm0 -- odwrócenie wyniku ret
Można również skorzystać z rozkazu pcmpgt, ale wymaga to przekształceń danych wejściowych, przez co kod jest koszmarnie wolny. Proszę z resztą spróbować zaimplementować poniższą tożsamość:
A = (A_hi << 4) | A_lo B = (B_hi << 4) | B_lo A > B <=> ((A_hi > B_hi) && (A_hi != B_hi)) || ((A_lo > B_lo) && (A_hi == B_hi))
; mask[9] = { 0x0000000000000000, ; 0x00000000000000ff, ; 0x000000000000ffff, ; 0x0000000000ffffff, ; 0x00000000ffffffff, ; 0x000000ffffffffff, ; 0x0000ffffffffffff, ; 0x00ffffffffffffff, ; 0xffffffffffffffff} ; ; wejście: ; cl - indeks ; wyjście: ; mm0 = mask[cl] ; niszczy: ; ecx, mm1 mask: movzx ecx, cl neg ecx lea ecx, [ecx*8 + 64] ; ecx = 64-8*zero_extend(cl) movd mm1, ecx pcmpeqb mm0, mm0 ; mm0 = pb(0xff) psrlq mm0, mm1 ret
; wejście: ; esi - adres ; ecx - ilość bajtów ; al - zliczany bajt ; wyjście: ; eax - ilość bajtów ; niszczy: ; mm0, mm1, mm2, mm3, mm4 bcount: pxor mm1, mm1 ; licznik pxor mm0, mm0 ; mm1 := pb(0) mov ah, al ; movd mm2, eax ; punpcklwd mm2, mm2 ; punpckldq mm2, mm2 ; mm2 := pb(al) push ecx shr ecx, 3 ; liczba 8-bajtowych bloków jz .skip1 .count: ; np. mm2 = |aa|aa|aa|aa|aa|aa|aa|aa| movq mm3, [esi] ; mm3 = |bb|aa|aa|cc|dd|aa|ee|aa| pcmpeqb mm3, mm2 ; mm3 = |00|ff|ff|00|00|ff|00|ff| psadbw mm3, mm0 ; mm3 = 0 +1 +1 +0 +0 +1 +0 +1 = 4 paddd mm1, mm3 ; mm1 += mm3 add esi, byte 8 loop .count .skip1: pop ecx and ecx, 0x7 ; liczba bajtów niemieszczących się w bloku jz .skip2 neg ecx ; lea ecx, [ecx*8 + 64] ; movd mm3, ecx ; pcmpeq mm4, mm4 ; psrlq mm4, mm3 ; maska ; np. mm4 = |00|00|00|ff|ff|ff|ff|ff| movq mm3, [esi] ; mm3 = |aa|cc|aa|aa|aa|bb|aa|dd| pcmpeqb mm3, mm2 ; mm3 = |ff|00|ff|ff|ff|00|ff|00| pand mm3, mm4 ; mm3 = |00|00|00|ff|ff|00|ff|00| psadbw mm3, mm0 ; mm3 = 3 paddd mm1, mm3 .skip2: movd eax, mm1 ret
Oczywiście pomijam tutaj standardową metodę ze zmienną pomocniczą — załóżmy że nie ma żadnego wolnego rejestru. Najprościej jest użyć poniższego sposobu:
pxor mm0, mm1 ; a = a xor b pxor mm1, mm0 ; b = b xor a pxor mm0, mm1 ; a = a xor b
Można także wykorzystać fakt, że rejestry MMX są mapowane na rejestry FPU i użyć rozkazu fxch. Ponieważ ten rozkaz zamienia st0 (czyli mm0) z dowolnym innym rejestrem tak więc tylko w takim przypadku jego użycie będzie efektywne, np.:
fxch st5 ; wymiana zawartości mm0 i mm5
Podobnie rozkazy finstp oraz fdecstp nie wpływają na zawartość rejestrów. Można je użyć do szybkiego przesunięcia zawartości rejestrów. Np. fincstp jest równoważne:
movq [temp], mm0 movq mm0, mm1 movq mm1, mm2 ... movq mm6, mm7 movq mm7, [temp]
Poniżej zestawienie kilku metod generacji stałych w rejestrach MMX. Oczywiście niekiedy potrzebny jest wybór rozmiaru spakowanych elementów.
pxor mmxreg, mmxreg psubb mmxreg, mmxreg pcmpgtb mmxreg, mmxreg
pcmpeqb mmxreg, mmxreg
pxor mm0, mm0 ; mm0 = packed_byte( 0) pcmpeqb mm1, mm1 ; mm1 = packed_byte(-1) psubb mm0, mm1 ; mm0 = packed_byte(+1)
Następnie, używając przesunięć bitowych, można ustawić dowolny bit.
pcmpeqw mm0, mm0 ; mm0 = packed_word(0xffff) -- -1 psllw mm0, 8 ; mm0 = packed_word(0xff00) -- -256 packsswb mm0, mm0 ; mm0 = packed_byte(0x80) -- saturate_sw2sb(-256) = -128
; packed_byte -- n w zakresie 0..7 ; np. n=6 pcmpeqw mm0, mm0 ; mm0 = packed_word(0b1111111111111111) psrlw mm0, 16-n ; mm0 = packed_word(0b0000000000111111) packsswb mm0, mm0 ; mm0 = packed_byte(0b00111111) ; packed_word -- n w zakresie 0..15 ; np. n=11 pcmpeqw mm0, mm0 ; mm0 = packed_word(0b1111111111111111) psrlw mm0, 16-n ; mm0 = packed_word(0b0000011111111111) ; packed_word -- n w zakresie 0..31 pcmpeqw mm0, mm0 psrlw mm0, 32-n
; packed_byte - n w zakresie 1..7 ; np. n==3 pcmpeqw mm0, mm0 ; mm0 = packed_word(0b1111111111111111) psllw mm0, 8-n ; mm0 = packed_word(0b1111111111111000) ; mm0 = |0xfff8|0xfff8|0xfff8|0xfff8| punpckbw mm0, mm0 ; mm0 = |0xffff|0xf8f8|0xffff|0xf8f8| punpckbw mm0, mm0 ; mm0 = |0xf8f8|0xf8f8|0xf8f8|0xf8f8| ; packed_word - n w zakresie 0..16 pcmpeqw mm0, mm0 psllw mm0, 16-n ; packed_dword - n w zakresie 0..32 pcmpeqd mm0, mm0 pslld mm0, 32-n
pcmpeqb mm1, mm1 ; mm1 = 0000000000000000 = packed_byte(0) pxor mm0, mm0 ; mm0 = ffffffffffffffff = packed_byte(-1) psubb mm0, mm1 ; mm0 = 0101010101010101 = packed_byte(1) movq mm1, mm0 ; psrlq mm1, 8 ; mm1 = |1|1|1|1|1|1|1|0| paddb mm0, mm1 ; mm0 = |2|2|2|2|2|2|2|1| movq mm1, mm0 psrlq mm1, 16 ; mm0 = |2|2|2|2|2|1|0|0| paddb mm0, mm1 ; mm0 = |4|4|4|4|4|3|2|1| movq mm1, mm0 psrlq mm1, 32 ; mm0 = |4|3|2|1|0|0|0|0| paddb mm0, mm1 ; mm0 = |8|7|6|5|4|3|2|1|
W APJ#5 Chris Dragan pokazał szybki sposób (przede wszystkim bez rozgałęzień) na moduł liczby U2:
; eax - liczba ze znakiem mov edx, eax ; sar edx, 31 ; edx = (eax < 0) ? 0xffffffff : 0x00000000 xor eax, edx ; eax = not eax sub eax, edx ; eax += 1
Zamiast sekwencji mov edx, eax/sar edx, 31 lepiej użyć rozkazu cdq.
Używając tej metody można bardzo szybko liczyć moduły liczb 16 i 32 bitowych w rejestrach MMX/SSE, np.:
; mm0 - wektor słów pabssw: movq mm1, mm0 psraw mm1, 15 pxor mm0, mm1 psubw mm0, mm1 ret
; mm0 - wektor podwójnych słów pabssd: movq mm1, mm0 psrad mm1, 15 pxor mm0, mm1 psubd mm0, mm1 ret
Poniżej szybszy sposób na obliczenie modułu słów (wykorzystuje rozkaz SSE).
; mm0 - wektor słów pabssw: pxor mm1, mm1 ; mm1 = packed_word(0x0000) psubw mm1, mm0 ; mm1 = -mm0 pmaxsw mm0, mm1 ; mm0[i] = (mm1[i] > mm0[i]) ? mm1[i] : mm0[i] ; i - numer słowa (0..3) ret
Moduły liczb 8-bitowych są trudniejsze do obliczenia przy użyciu pierwszego sposobu, z tego względu, że nie ma odpowiednika rozkazów psraw.
segment .data msb db 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 segment .text ; mm0 - wektor bajtów ze znakiem pabssb1: movq mm1, mm0 ; pand mm1, [msb] ; pcmpeqb mm1, [msb] ; mm1[i] = (mm0[i] < 0) ? 0xff : 0x00 ; i - numer bajtu (0..7) pxor mm0, mm1 psubb mm0, mm1 ret pabssb2: pxor mm1, mm1 ; mm1 = packed_byte(0) pcmpgtb mm1, mm0 ; mm1[i] = (0 > mm0[i]) ? 0xff : 0x00 pxor mm0, mm1 psubb mm0, mm1 ret
Ale gdy jest możliwość użycia rozkazu SSE pminub — minimum, bowiem wartość liczona w NKB dla liczby ujemnej (U2) jest większa od liczby dodatniej (U2).
pabssb3: pxor mm1, mm1 ; mm1 = packed_byte(0x00) psub mm1, mm0 ; mm1[i] -= mm0[i] ; i=0..7 pminub mm0, mm1 ret