MMX&SSE snippets

Autor: Wojciech Muła
Dodany:9.02.2002
Aktualizacja:28.10.2002

Treść

packluww — Pack Low Unsigned Word to Words

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|

packlww — Pack Low Signed Word to Words

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|

psumwd — suma słów bez znaku

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

dot_product (iloczyn skalarny) 3DNow!

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     |

pcmpgtu

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))

Generacja maski

; 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

bcount — zliczanie bajtów

; 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

Wymiana zawartości rejestrów MMX

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]

Generowanie stałych w rejestrach MMX

Poniżej zestawienie kilku metod generacji stałych w rejestrach MMX. Oczywiście niekiedy potrzebny jest wybór rozmiaru spakowanych elementów.

Zerowanie

pxor    mmxreg, mmxreg
psubb   mmxreg, mmxreg
pcmpgtb mmxreg, mmxreg

-1 (0xff)

pcmpeqb  mmxreg, mmxreg

1 (0x01)

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.

-128 (0x80)

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

n najmłodszych bitów ustawionych

; 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

n najstarszych bitów ustawionych

; 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

0x0807060504030201

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|

Moduł liczby

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.

pabssw/pabssd

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

pabssb

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