20 maja 2009

SSE w praktyce - #1

Dziś postaram się przedstawić podstawowe operacje jakie możemy wykonać przy użyciu multimedialnych rozszerzeń procesora. Pokażę możliwość bezpośredniego wykorzystania assemblera, zaś w następnych notkach skupię się na wewnętrznych poleceniach kompilatora, specjalnie przygotowanych do obsługi SSE. Aby prezentowany kod mógł zostać uruchomiony potrzebny jest procesor klasy Intel Pentium III lub lepszy.
Więc do dzieła ;)

Jednak, jak to zwykle bywa, na początek jeszcze trochę teorii.
Programując przy użyciu SSE mamy do dyspozycji 8 dodatkowych, 128 bitowych rejestrów (w przypadku kodu dla procesorów 64 bitowych, liczba ta zwiększa sie do 16) - od xmm0 do xmm7. Wszystkie operacje, jakie będziemy chcieli wykonać przy użyciu omawianych tu instrukcji multimedialnych, będą operować właśnie na tych rejestrach. Jak już wiadomo, każdy z nich może przechować 4 wartości zmiennoprzecinkowe pojedynczej precyzji (float).

Na początek przygotujmy dane na których będziemy pracować: czteroelementowa, jednowymiarowa, wyrównana do 128 bitów tablica typu float – czyli nasz wektor.

__declspec(align(16)) float gVector4[4];
Zobaczmy na początek, w jaki sposób możemy dostać się do danych naszego wektora. Nie będziemy tego robić standardowo,
za pomocą operatora [ ] – posłużymy się do tego odpowiednimi instrukcjami SSE.
Pierwszą podstawową rzeczą, będzie skopiowanie zawartości komórki pamięci, gdzie znajduje się nasz wektor, do rejestru xmm.
_asm{
    lea esi,gVector4
    movaps xmm0,[esi]
    //wykonaj operacje...
    movaps [esi],xmm0
}
Pierwsza linijka odpowiada za pobranie adresu zmiennej gVector4 (dokładny opis działania instrukcji lea można znaleźć tu)
i skopiowanie go do rejestru esi (wskaźnik danych).
Następnie możemy zobaczyć już instrukcję z rodziny SSE –
movaps. Odpowiada ona za kopiowanie zawartości rejestrów xmm
między sobą, a także pamięcią. W tym przypadku, zawartość komórki pamięci na który wskazuje rejestr esi, zostanie przesłana do rejestru xmm0.
Ostatnia instrukcja kopiuje zawartość rejestru xmm0 pod adres wskazywany przez esi – czyli do naszej zmiennej.
Jak widać, mimo że jest to assembler, nie jest to takie trudne. Wygenerujmy więc nieco więcej bardziej praktycznego kodu.
Jak pewnie zważyliście, deklarowanie za każdym razem owej specyficznej tablicy z pewnością nie należy do specjalnie wygodnych.
Uprośćmy więc nieco ten proces:
typedef __declspec(align(16)) float AlignedVector4D[4];
Ten niewielki kawałek kodu, z pewnością pozytywnie wpłynie na czytelność kodu.
Poznajmy więc kilka podstawowych operacji: dodawanie, odejmowanie, mnożenie i dzielenie.
Na początek zdefiniujmy dwa wektory, na których będziemy wykonywać nasze operacje:
AlignedVector4D vec1 = {5.0,6.0,3.0,1.0};
AlignedVector4D vec2 = {2.5,3.0,1.5,0.5};

Za dodawanie odpowiada instrukcja addps. Przykład użycia:
_asm{
    lea esi,[vec1];
    lea edi,[vec2];

    movaps xmm0,[esi];
    movaps xmm1,[edi];

    addps xmm0,xmm1;

    movaps [esi],xmm0
}
Jak widać kod jest bardzo prosty. Dwie początkowe instrukcje odpowiadają za pobranie adresu zmiennych, następnie kopiujemy je
do rejestrów xmm0 oraz xmm1 by dodać je do siebie w następnej instrukcji. Wynik zapisywany jest do xmm0 które jest kopiowane
pod adres jaki wskazuje esi, czyli do zmiennej vec1.
Zauważmy, że obecność kopiowania do rejestru xmm1 nie jest konieczna – addps, podobnie jak movaps może pracować na jednym
operandzie znajdującym sie w pamięci operacyjnej:
_asm{
    lea esi,[vec1];
    lea edi,[vec2];

    movaps xmm0,[esi];
    addps xmm0,[edi];
    movaps [esi],xmm0
}
Pozostałe operacje wykonuje się analogicznie:
subps – odejmowanie
mulps – mnożenie
divps – dzielenie
To byłoby na tyle. W kolejnych częściach poznamy trochę więcej interesujących rzeczy ;)

3 komentarze:

  1. Wygodniejsze są instrukcje typu intrinsics, z którymi kompilator potrafi czynić cuda. Ogólnie opłaca się uzywać tego dodatkowego zestawu instrukcji, jeżeli koszt załadowania wektora nie jest większy o kosztu wykonania tych instrukcji. Najlepiej łączyć kilka operacji, wtedy czuć wydajność :)

    OdpowiedzUsuń
  2. Zgadza się. Trochę wiedzy jednak jak to działa wewnątrz nie zaszkodzi ;). Poza tym, niektóre intrinsics, oznaczone jako Composite, nie odpowiadają jednej instrukcji w asm, choć faktycznie, lepiej napisać tego chyba już się nie da :)

    OdpowiedzUsuń
  3. Dobry post :) Oby więcej takich!

    OdpowiedzUsuń