   #Start Prev Next Contents

   Jak pisac programy w jezyku asembler pod Linuksem?

Czesc 6 - SIMD, czyli jak dziala MMX

   - A coz to takiego to SIMD ?! - zapytacie.
   Juz odpowiadam.
   SIMD = Single Instruction, Multiple Data = jedna instrukcja, wiele
   danych.
   Jest to technologia umozliwiajaca jednoczesne wykonywanie tej samej
   instrukcji na kilku wartosciach. Na pewno znany jest wam co najmniej
   jeden przyklad zastosowania technologii SIMD. Jest to MultiMedia
   Extensions, w skrocie MMX u Intela, a 3DNow! u AMD. Innym mniej znanym
   zastosowaniem jest SSE, ktore omowie pozniej.
   Zaczniemy od omowienia, jak wlasciwie dziala to cale MMX.
     _________________________________________________________________

MMX / 3DNow!

   Technologia MMX operuje na 8 rejestrach danych, po 64 bity kazdy,
   nazwanych mm0 ... mm7. Niestety, rejestry te nie sa "prawdziwymi"
   (oddzielnymi) rejestrami - sa czesciami rejestrow koprocesora (ktore,
   jak pamietamy, maja po 80 bitow kazdy). Pamietajcie wiec, ze nie mozna
   naraz wykonywac operacji na FPU i MMX/3DNow!.

   Rejestry 64-bitowe sluza do umieszczania w nich danych spakowanych. Na
   czym to polega? Zamiast miec na przyklad 32 bity w jednym rejestrze,
   mozna miec dwa razy po 32. Tak wiec rejestry mieszcza 2 podwojne slowa
   (dword, 32 bity) lub 4 slowa (word, 16 bitow) lub az 8 spakowanych
   bajtow.

   Zajmijmy sie omowieniem instrukcji operujacych na tych rejestrach.
   Instrukcje MMX mozna podzielic na kilka grup (nie wszystkie instrukcje
   beda tu wymienione):
     * instrukcje transferu danych:
          + MOVD mmi, rej32/mem32 (i=0,...,7)
          + MOVQ mmi, mmj/mem64 (i,j=0,...,7)
     * instrukcje arytmetyczne:
          + dodawanie normalne: PADDB (bajty) / PADDW (slowa) / PADDD
            (dwordy)
          + dodawanie z nasyceniem ze znakiem: PADDSB (bajty) / PADDSW
            (slowa).
            Jezeli wynik przekracza 127 lub 32767 (bajty/slowa), to jest
            do tej wartosci zaokraglany, a NIE jest tak, ze nagle zmienia
            sie na ujemny. Daje to lepszy efekt, na przyklad w czasie
            sluchania muzyki czy ogladania filmu. Hipotetyczny przyklad:
            2 kolory "szare" z dadza w sumie "czarny" a nie cos posrodku
            skali kolorow.
          + dodawanie z nasyceniem bez znaku: PADDUSB / PADDUSW.
            Jezeli wynik przekracza 255 lub 65535, to jest do tej
            wartosci zaokraglany.
          + odejmowanie normalne: PSUBB (bajty) / PSUBW (slowa) / PSUBD
            (dwordy)
          + odejmowanie z nasyceniem ze znakiem: PSUBSB (bajty) / PSUBSW
            (slowa).
            Jesli wynik jest mniejszy niz -128 lub -32768 to jest do tej
            wartosci zaokraglany.
          + odejmowanie z nasyceniem bez znaku: PSUBUSB (bajty) / PSUBUSW
            (slowa)
            Jesli wynik jest mniejszy niz 0, to staje sie rowny 0.
          + mnozenie:
               o PMULHRWC, PMULHRIW, PMULHRWA - mnozenie spakowanych
                 slow, zaokraglanie, zapisanie tylko starszych 16 bitow
                 wyniku (z 32).
               o PMULHUW - mnozenie spakowanych slow bez znaku,
                 zachowanie starszych 16 bitow
               o PMULHW, PMULLW - mnozenie spakowanych slow bez znaku,
                 zapisanie starszych/mlodszych 16 bitow (odpowiednio).
               o PMULUDQ - mnozenie spakowanych dwordow bez znaku
          + mnozenie i dodawanie: PMADDWD - do mlodszego dworda rejestru
            docelowego idzie suma iloczynow 2 najmlodszych slow ze soba i
            2 starszych (bity 16-31) slow ze soba. Do starszego dworda -
            suma iloczynow 2 slow 32-47 i 2 slow 48-63.
     * instrukcje porownawcze:
       Zostawiaja w odpowiednim bajcie/slowie/dwordzie same jedynki
       (FFh/FFFFh/FFFFFFFFh) gdy wynik porownania byl prawdziwy, same
       zera - gdy falszywy.
          + na rownosc PCMPEQB / PCMPEQW / PCMPEQD (EQ oznacza rownosc)
          + na "wieksze niz": PCMPGTPB / PCMPGTPW / PCMPGTPD (GT oznacza
            greater than, czyli wiekszy)
     * instrukcje konwersji:
          + pakowanie: PACKSSWB / PACKSSDW, PACKUSWB - "upychaja"
            slowa/dwordy do bajtow/slow i pozostawiaja w rejestrze
            docelowym.
          + rozpakowania starszych czesci (unpack high): PUNPCKHBW,
            PUNPCKHWD, PUNPCKHDQ - pobieraja starsze czesci
            bajtow/slow/dwordow z jednego i drugiego rejestru, mieszaja
            je i zostawiaja w pierwszym.
          + rozpakowania mlodszych czesci (unpack low): PUNPCKLBW,
            PUNPCKLWD, PUNPCKLDQ - jak wyzej, tylko pobierane sa mlodsze
            czesci
     * instrukcje logiczne:
          + PAND (bitowe AND)
          + PANDN (najpierw bitowe NOT pierwszego rejestru, potem jego
            bitowe AND z drugim rejestrem)
          + POR (bitowe OR)
          + PXOR (bitowe XOR)
     * instrukcje przesuniecia (analogiczne do znanych SHL, SHR i SAR,
       odpowiednio):
          + w lewo: PSLLW (slowa) / PSLLD (dword-y), PSLLQ (qword)
          + w prawo, logiczne: PSRLW (slowa) / PSRLD (dword-y), PSRLQ
            (qword)
          + w prawo, arytmetyczne: PSRAW (slowa)/ PSRAD (dword-y)
     * instrukcje stanu MMX:
          + EMMS - Empty MMX State - ustawia rejestry FPU jako wolne,
            umozliwiajac ich uzycie. Ta instrukcja musi byc wykonana za
            kazdym razem, gdy konczymy prace z MMX i chcemy zaczac prace
            z FPU.

   Rzadko ktora z tych instrukcji traktuje rejestr jako calosc, czesciej
   operuja one na poszczegolnych wartosciach osobno, rownolegle.

   Sprobuje teraz podac kilka przykladow zastosowania MMX.
     _________________________________________________________________

   Przyklad 1. Dodawanie dwoch tablic bajtow w pamieci. Bez MMX mogloby
   to wygladac mniej wiecej tak:
   (przeskocz dodawanie tablic)
; EDX - adres pierwszej tablicy bajtow
; ESI - adres drugiej tablicy bajtow
; EDI - adres docelowej tablicy bajtow
; ECX - liczba bajtow w tablicach. Przyjmiemy, ze rozna 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         ; zwieksz o 1 indeksy tablic
                inc esi
                inc edi
                loop petla      ; dzialaj, dopoki ECX rozne od 0.
     _________________________________________________________________

   A z MMX:
   (przeskocz dodawanie tablic z MMX)
        mov ebx, ecx    ; EBX = liczba bajtow
        and ebx, 7      ; bedziemy brac po 8 bajtow - obliczamy
                        ; wiec reszte z dzielenia przez 8

        shr ecx, 3      ; dzielimy ECX przez 8
petla:
        movq mm0, [edx] ; pobierz 8 bajtow z pierwszej tablicy
        paddb mm0, [esi]; dodaj 8 spakowanych bajtow z drugiej
        movq [edi], mm0 ; zapisz 8 bajtow w tablicy docelowej
        add edx, 8      ; zwieksz indeksy do tablic o 8
        add esi, 8
        add edi, 8
        loop petla      ; dzialaj, dopoki ECX rozne od 0.

        test ebx, ebx   ; czy EBX = 0?
        jz koniec       ; jesli tak, to juz skonczylismy

        mov ecx, ebx    ; ECX = resztka, co najwyzej 7 bajtow.
                        ; 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         ; zwieksz o 1 indeksy do tablic
        inc esi
        inc edi
        loop petla2     ; dzialaj, dopoki ECX rozne od 0
koniec:

        emms            ; wyczysc rejestry MMX, by FPU moglo z nich korzystac

   Podobnie beda przebiegac operacje PAND, POR, PXOR, PANDN.

   Przy duzych ilosciach danych, sposob drugi bedzie wykonywal okolo 8
   razy mniej instrukcji niz pierwszych, bo dodaje na raz 8 bajtow. I o
   to wlasnie chodzilo.
     _________________________________________________________________

   Przyklad 2. Kopiowanie pamieci.
   Bez MMX:
   (przeskocz kopiowanie pamieci)
; DS:ESI - zrodlo
; ES:EDI - cel
; ECX - liczba bajtow
        mov ebx, ecx    ; EBX = liczba bajtow
        and ebx, 3      ; EBX = reszta z dzielenia liczby bajtow przez 4
        shr ecx, 2      ; ECX = liczba bajtow dzielona przez 4

        cld             ; kierunek: do przodu
        rep movsd       ; dword z DS:ESI idzie pod ES:EDI, EDI:=EDI+4,
                        ; ESI:=ESI+4, dopoki ECX jest rozny od 0
        mov ecx, ebx    ; ECX = liczba pozostalych bajtow
        rep movsb       ; resztke kopiujemy po bajcie
     _________________________________________________________________

   Z MMX:
   (przeskocz kopiowanie pamieci z MMX)
                mov ebx, ecx    ; EBX = liczba bajtow
                and ebx, 7      ; EBX = reszta z dzielenia liczby bajtow
                                ; przez 8

                shr ecx, 3      ; ECX = liczba bajtow dzielona przez 8
        petla:
                movq mm0, [esi] ; MM0 = 8 bajtow z tablicy pierwszej
                movq [edi], mm0 ; kopiujemy zawartosc MM0 pod [EDI]
                add esi, 8      ; zwiekszamy indeksy tablic o 8
                add edi, 8
                loop petla      ; dzialaj, dopoki ECX rozne od 0

                mov ecx, ebx    ; ECX = liczba pozostalych bajtow
                cld             ; kierunek: do przodu
                rep movsb       ; resztke kopiujemy po bajcie

                emms            ; wyczysc rejestry MMX

   lub, dla solidniejszych porcji danych:
   (przeskocz kolejne kopiowanie pamieci)
                mov ebx, ecx    ; EBX = liczba bajtow
                and ebx, 63     ; EBX = reszta z dzielenia liczby bajtow
                                ; przez 64
                shr ecx, 6      ; ECX = liczba bajtow dzielona przez 64
        petla:
                ; kopiuj 64 bajty spod [ESI] do rejestrow 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 rejestrow 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     ; zwieksz indeksy do tablic o 64
                add edi, 64
                loop petla      ; dzialaj, dopoki ECX rozne od 0

                mov ecx, ebx    ; ECX = liczba pozostalych bajtow
                cld             ; kierunek: do przodu
                rep movsb       ; resztke kopiujemy po bajcie

                emms            ; wyczysc rejestry MMX
     _________________________________________________________________

   Przyklad 3. "Rozmnozenie" jednego bajtu na caly rejestr MMX.
   (przeskocz rozmnazanie bajtu)
; format ELF executable         ; tylko dla FASMa
; entry _start

; FASM: segment readable executable
section .text

global _start                   ; FASM: usunac te linijke

_start:

        movq mm0, [wart1]       ; mm0 = 00 00 00 00 00 00 00 33
                                ;  (33h = kod ASCII cyfry 3)

        punpcklbw mm0, mm0      ; do najmlodszego slowa wloz najmlodszy bajt
                                ; mm0 i najmlodszy bajt mm0 (czyli ten sam)
                                ; mm0 = 00 00 00 00 00 00 33 33

        punpcklwd mm0, mm0      ; do najmlodszego dworda wloz dwa razy
                                ; najmlodsze slowo mm0
                                ; mm0 = 00 00 00 00 33 33 33 33

        punpckldq mm0, mm0      ; do najmlodszego (i jedynego) qworda wloz 2x
                                ; najmlodszy dword mm0 obok siebie
                                ; mm0 = 33 33 33 33 33 33 33 33

        movq [wart2], mm0

        emms                    ; wyczysc rejestry MMX

        mov     eax, 4
        mov     ebx, 1
        mov     ecx, wart2
        mov     edx, 9          ; wartosc2 + znak nowej linii
        int     80h             ; wyswietl

        mov     eax, 1
        xor     ebx, ebx
        int     80h

; FASM: segment readable writeable
section .data

wart1:  db      "3"
        times 7 db 0                    ; trojka i 7 bajtow zerowych

wart2:  times   8       db      2       ; 8 bajtow o wartosci 2 != 33h

nowa_linia      db      0ah

   Kompilujemy, uruchamiamy i ... rzeczywiscie na ekranie pojawia sie
   upragnione osiem trojek!

   Technologia MMX moze byc uzywana w wielu celach, ale jej najbardziej
   korzystna cecha jest wlasnie rownoleglosc wykonywanych czynnosci,
   dzieki czemu mozna oszczedzic czas procesora.
     _________________________________________________________________

Technologia SSE

   Streaming SIMD Extensions (SSE), Pentium III lub lepszy oraz najnowsze
   procesory AMD

   Streaming SIMD Extensions 2 (SSE 2), Pentium 4 lub lepszy oraz AMD64

   Streaming SIMD Extensions 3 (SSE 3), Xeon lub lepszy oraz AMD64

   Krotko mowiac, SSE jest dla MMX tym, czym FPU jest dla CPU. To znaczy,
   SSE przeprowadza rownolegle operacje na liczbach ulamkowych.
   SSE operuje juz na calkowicie osobnych rejestrach nazwanych xmm0, ...,
   xmm7 po 128 bitow kazdy. W trybie 64-bitowym dostepne jest dodatkowych
   8 rejestrow: xmm8, ..., xmm15.
   Prawie kazda operacja zwiazana z danymi w pamieci musi miec te dane
   ustawione na 16-bajtowej granicy, czyli jej adres musi sie dzielic
   przez 16. Inaczej generowane jest przerwanie (wyjatek).

   SSE 2 rozni sie od SSE kilkoma nowymi instrukcjami konwersji
   ulamek-liczba calkowita oraz tym, ze moze operowac na liczbach
   ulamkowych rozszerzonej precyzji (64 bity).

   U AMD czesciowo 3DNow! operuje na ulamkach, ale co najwyzej na dwoch
   gdyz sa to rejestry odpowiadajace MMX, a wiec 64-bitowe. 3DNow! Pro
   jest odpowiednikiem SSE w procesorach AMD. Odpowiedniki SSE2 i SSE3
   pojawily sie w AMD64.

   Instrukcje SSE (nie wszystkie beda wymienione):
     * Przemieszczanie danych:
          + MOVAPS - move aligned packed single precision floating point
            values - przemiesc ulozone (na granicy 16 bajtow) spakowane
            ulamki pojedynczej precyzji (4 sztuki po 32 bity)
          + MOVUPS - move unaligned (nieulozone) packed single precision
            floating point values
          + MOVSS - move scalar (1 sztuka, najmlodsze 32 bity rejestru)
            single precision floating point value
     * Arytmetyczne:
          + ADDPS - add packed single precision floating point values =
            dodawanie czterech ulamkow do czterech
          + ADDSS - add scalar single precision floating point values =
            dodawanie jednego ulamka do innego
          + MULPS - mnozenie spakowanych ulamkow, rownolegle, 4 pary
          + MULSS - mnozenie jednego ulamka przez inny
          + DIVPS - dzielenie spakowanych ulamkow, rownolegle, 4 pary
          + DIVSS - dzielenie jednego ulamka przez inny
          + obliczanie odwrotnosci ulamkow, ich pierwiastkow, odwrotnosci
            pierwiastkow, znajdowanie wartosci najwiekszej i najmniejszej
     * Logiczne:
          + ANDPS - logiczne AND spakowanych wartosci (ale oczywiscie tym
            bardziej zadziala dla jednego ulamka w rejestrze)
          + ANDNPS - AND NOT (najpierw bitowe NOT pierwszego rejestru,
            potem jego bitowe AND z drugim rejestrem) dla spakowanych
          + ORPS - OR dla spakowanych
          + XORPS - XOR dla spakowanych
     * Instrukcje porownania: CMPPS, CMPSS, (U)COMISS
     * Instrukcje tasowania i rozpakowywania. Podobne dzialanie jak
       odpowiadajace instrukcje MMX.
     * Instrukcje konwersji z ulamkow na liczby calkowite i na odwrot.
     * Instrukcje operujace na liczbach calkowitych 64-bitowych (lub
       128-bitowych w SSE 2)

   W wiekszosci przypadkow instrukcje dodane w SSE 2 roznia sie od
   powyzszych ostatnia litera, ktora jest D, co oznacza "double
   precision", na przyklad MOVAPD.
     _________________________________________________________________

   No i krotki przykladzik. Inna wersja procedury do kopiowania pamieci.
   Tym razem z SSE.
   (przeskocz kopiowanie pamieci z SSE)
; Tylko jesli ESI i EDI dzieli sie przez 16! Inaczej uzywac MOVUPS.
                mov ebx, ecx    ; EBX = liczba bajtow
                and ebx, 127    ; EBX = reszta z dzielenia liczby bajtow
                                ; przez 128
                shr ecx, 7      ; ECX = liczba bajtow dzielona przez 128
        petla:
                ; kopiuj 128 bajtow spod [ESI] do rejestrow 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 bajtow z rejestrow 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    ; zwieksz indeksy do tablic o 128
                add edi, 128
                loop petla      ; dzialaj, dopoki ECX rozne od 0

                mov ecx, ebx    ; ECX = liczba pozostalych bajtow
                cld             ; kierunek: do przodu
                rep movsb       ; resztke kopiujemy po bajcie

   Nie jest to ideal, przyznaje. Mozna bylo na przyklad uzyc instrukcji
   wspierajacych pobieranie danych z pamieci: PREFETCH.
     _________________________________________________________________

   A teraz cos innego: rozdzielanie danych. Przypuscmy, ze z jakiegos
   urzadzenia (lub pliku) czytamy bajty w postaci XYXYXYXYXY..., a my
   chcemy je rozdzielic na dwie tablice, zawierajace tylko XXX... i
   YYY... (oczywiscie bajty moga miec rozne wartosci, ale idea jest taka,
   ze co drugi chcemy miec w drugiej tablicy). Oto, jak mozna tego
   dokonac z uzyciem SSE2. To jest tylko fragment programu.
   (przeskocz rozdzielanie bajtow)
        mov     eax, 4                  ; funkcja zapisu do pliku
        mov     ebx, 1                  ; na stdout (ekran)
        mov     ecx, dane_pocz
        mov     edx, dane_pocz_dl
        int     80h

        mov     eax, 4                  ; funkcja zapisu do pliku
        mov     ebx, 1                  ; na stdout (ekran)
        mov     ecx, dane
        mov     edx, dane_dl
        int     80h                     ; wypisz dane poczatkowe


; FASM: "movaps         xmm0, dqword [dane]"
        movaps          xmm0, [dane]
        movaps          xmm1, xmm0
                ; XMM1=XMM0 = X1Y1 X2Y2 X3Y3 X4Y4 X5Y5 X6Y6 X7Y7 X8Y8
                ; XXM* musza zawierac tylko po jednym bajcie w kazdym slowie

        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 juz nie maja adresu
                                ; podzielnego przez 16,
                                ; wiec nie mozna uzyc MOVAPS
                                ; a my i tak chcemy tylko 8 bajtow
; FASM: "movq   qword [dane1], xmm1"
        movq    [dane1], xmm1

        mov     eax, 4                  ; funkcja zapisu do pliku
        mov     ebx, 1                  ; na stdout (ekran)
        mov     ecx, dane_kon
        mov     edx, dane_kon_dl
        int     80h

        mov     eax, 4                  ; funkcja zapisu do pliku
        mov     ebx, 1                  ; na stdout (ekran)
        mov     ecx, dane1
        mov     edx, dane1_dl
        int     80h                     ; wypisz pierwsze dane koncowe

        mov     eax, 4                  ; funkcja zapisu do pliku
        mov     ebx, 1                  ; na stdout (ekran)
        mov     ecx, dane2
        mov     edx, dane2_dl
        int     80h                     ; wypisz drugie dane koncowe

        mov     eax, 1
        xor     ebx, ebx
        int     80h



section .data
        ; FASM: segment readable writeable

align   16                      ; dla SSE

dane            db      "ABCDEFGHIJKLMNOP", 10
        ; FASM: "=" zamiast "equ"
dane_dl         equ     $ - dane

dane1           db      0, 0, 0, 0, 0, 0, 0, 0, 10, 9
        ; FASM: "=" zamiast "equ"
dane1_dl        equ     $ - dane1

dane2           db      0, 0, 0, 0, 0, 0, 0, 0, 10
        ; FASM: "=" zamiast "equ"
dane2_dl        equ     $ - dane2

dane_pocz db "Program demonstrujacy SSE. Dane na poczatku: ", 10, 9
        ; FASM: "=" zamiast "equ"
dane_pocz_dl    equ     $ - dane_pocz

dane_kon        db      "Dane na koncu: ", 10, 9
        ; FASM: "=" zamiast "equ"
dane_kon_dl     equ     $ - dane_kon
     _________________________________________________________________

   Po szczegolowy opis wszystkich instrukcji odsylam, jak zwykle do
   Intela i AMD.

   Instrukcje typu SIMD wspomagaja szybkie przetwarzanie multimediow:
   dzwieku, obrazu. Omowienie kazdej instrukcji w detalu jest niemozliwe
   i niepotrzebne, gdyz szczegolowe opisy sa zamieszczone w ksiazkach
   Intela lub AMD.
   Milej zabawy.

   Poprzednia czesc kursu (klawisz dostepu 3)
   Kolejna czesc kursu (klawisz dostepu 4)
   Spis tresci off-line (klawisz dostepu 1)
   Spis tresci on-line (klawisz dostepu 2)
   Ulatwienia dla niepelnosprawnych (klawisz dostepu 0)
     _________________________________________________________________

Cwiczenia

    1. Z dwoch zmiennych typu qword wczytaj do dwoch dowolnych rejestrow
       MMX (ktore najlepiej od razu skopiuj do innych), po czym wykonaj
       wszystkie mozliwe dodawania i odejmowania. Wynik kazdego zapisz w
       oddzielnej zmiennej typu qword.
    2. Wykonaj operacje logiczne OR, AND i XOR na 64 bitach na raz
       (wczytaj je do rejestru MMX, wynik zapisz do pamieci).
    3. Wczytajcie do rejestru MMX wartosc szesnastkowa 30 31 30 31 30 31
       30 31, po czym wykonajcie rozne operacje rozpakowania i pakowania,
       zapiszcie i wyswietlcie wynik jak kazdy normalny ciag znakow.
    4. Wczytajcie do rejestrow XMM po 4 liczby ulamkowe dword, wykonajcie
       dodawania i odejmowania, po czym sprawdzcie wynik koprocesorem.
