| Autor: | Wojciech Muła |
|---|---|
| Dodany: | 9.02.2002 |
| Aktualizacja: | 28.10.2002 |
Contents
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