Warsztat - Programowanie gier

Lipiec 30, 2010, 17:33:44 *
Witamy, Gość. Zaloguj się, lub zarejestruj proszę.

Zaloguj się podając nazwę użytkownika, hasło i długość sesji
Aktualności: Warsztat, Regulamin forum, #warsztat, Wiki, FAQ, NoPaste, Mapa
 
   Strona główna   Pomoc Szukaj Zaloguj się Rejestracja  
Strony: 1 2 [3]
  Drukuj  
Autor Wątek: Wsparcie procesorów dla MMX, SSE itp.  (Przeczytany 5138 razy)
Esidar
SuperHero Member
******

wiadomości: 1359


Zobacz profil
« Odpowiedz #30 : Listopad 24, 2008, 04:47:46 »

Ten jeden cykl to jest trochę złudne.
Trudno nazwać procki x86 współczesnymi, a instrukcje SSE większością ;-)
ps: Masz może gdzieś porównanie czasu wykonywania się instrukcji dla proców AMD/Intel?

Dla intela manuale są tutaj http://www.intel.com/products/processor/manuals/ (Optimization Manual).

Co do throughput i lattency to mają je praktycznie wszystkie procki. Nie tylko x86 Smiley również PowerPC, ARM, GPU, itd. Do jednego cyklu można uprościć najwyżej proste ALU, operacje na int'ach, add, or, and, mov, itd (w prockach Intela, ALU chodzi na 2x większym zegarze niż reszta co daje 0,5 cykla na thoughput i latency). Wszystko inne wymaga kilku cykli do wykonania.

Cytuj
Masz na myśli zastosowanie shufps/movlhps/movhlps?
Chodzi o inną organizację danych i ich pobieranie. Przy wierszowym Vector, obliczenie dot wygląda tak:
Kod:
float x = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
jest to upierdliwa operacja bo mozna wykonac rownolegle tylko mnozenie. Jeżemy napiszemy sobie uniwersalny "dot" w klasie Vector to będzie wyglądać mniej więcej tak:
Kod:
// oblicz 1 dot
r1 = _mm_mul_ps( v1, v2 );
r1 = _mm_add_ps( r1, _mm_movehl_ps(r1, r1));
r1 = _mm_add_ps( r1, _mm_shuffle_ps(r1, r1, _MM_SHUFFLE(3, 2, 1, 1)));
_mm_store_ss( r1 ); // zapisz 1 float
I taki kod będzie się wykonywać dla każdego Dot, 1 mnożenie, 2 dodawania, 1 move, 1 shuffle.

Pojedyńczego Dot nie da się bardziej zoptymalizować, ale jeżeli potrzebujemy obliczyć 4 różne doty na raz, to mozemy je policzyć kolumnowo. Gdy powielimy 4x powyższy kod, to będziemy mieć 4 mnożenia, 8 dodawań, 4 move i 4 shuffle. Zamiast tego możemy zrobić obliczenia kolumnowe:
Kod:
// oblicz 4 doty jednoczesnie
r1 = _mm_mul_ps( k1, k2 );
r2 = _mm_mul_ps( k1, k3 );
r3 = _mm_mul_ps( k1, k4 );
r1 = _mm_add_ps( r1, r2 );
r1 = _mm_add_ps( r1, r3 );
_mm_store_ps( r1 ); // zapisz 4 floaty (4 wyniki dla 4 dot'ow)

Niestety wymaga to troche innej organizacji danych (SoA - structure of arrays, zamiast klasycznego AoS czyli Array of Structures). Czyli zamiast:
Kod:
float doty[] = {
 v1x, v1y, v1z, 0,
 v2x, v2y, v2z, 0,
 v3x, v3y, v3z, 0,
 v4x, v4y, v4z, 0,
};
Mamy
Kod:
float doty[] = {
 v1x, v2x, v3x, v4x,
 v1y, v2y, v3y, v4y,
 v1z, v2z, v3z, v4z
};

Dlatego najwięcej dające optymalizacje muszą być zaplanowane dużo wcześniej już na poziomie silnika a nie pojedyńczej klasy Vector czy Matrix.

W SSE4 w końcu intel poszedł po rozum do głowy, że może jednak ludziom nie utrudniać życia i wprowadził Dot. Ale na starszych SSE trzeba radzić sobie tak jak powyżej.
« Ostatnia zmiana: Listopad 24, 2008, 04:56:56 wysłane przez Esidar » Zapisane
Netrix
Hero Member
*****

wiadomości: 648


c++/c#/java


Zobacz profil WWW
« Odpowiedz #31 : Grudzień 02, 2008, 17:52:15 »

Jak to jest z dostępem bezpośrednim do unii __m128? Wiem, że jest to głównie abstrakcja dla kompilatora, żeby wiedział jak to co ja pisze ma grać oraz, ze lepiej niej nie zaglądać bezpośrednio. Zastanawia mnie jednak, czy nie szybciej jest po prostu odczytywać oraz zapisywać wartości (gdy jest potrzeba) bezpośrednio do obiektów w tej unii, niż korzystać z wyrafinowanych funkcji do wyciągania tych wartości z rejestrów.
Zapisane

“Ekspert to człowiek, który w bardzo wąskiej dziedzinie zrobił wszystkie możliwe błędy.” Niels Bohr.
http://netrix.org.pl
Reg
Member2000
*******

wiadomości: 3791



Zobacz profil WWW
« Odpowiedz #32 : Grudzień 02, 2008, 22:26:11 »

Tak, ja w swoim kodzie testowym używałem bezpośrednio rzutowania, o tak:

Kod:
const unsigned COUNT = 102400;
__declspec(align(16)) VEC4 IN1[COUNT], IN2[COUNT], OUT1[COUNT], OUT2[COUNT];

int main()
{
for (unsigned iter = 0; iter < 10; iter++)
{
for (unsigned i = 0; i < COUNT; i++)
{
IN1[i] = VEC4((float)i, (float)i, (float)i, (float)i);
IN2[i] = VEC4((float)i * 10.0f, (float)i * 12.0f, (float)i * 15.0f, (float)i * 20.0f);
}

{
PROFILE_GUARD("SSE, load+store");
__m128 SseIn1, SseIn2, SseOut;
for (unsigned i = 0; i < COUNT; i++)
{
SseIn1 = _mm_load_ps((float*)&IN1[i]);
SseIn2 = _mm_load_ps((float*)&IN2[i]);
SseOut = _mm_add_ps(SseIn1, SseIn2);
_mm_store_ps((float*)&OUT2[i], SseOut);
}
}

{
PROFILE_GUARD("SSE, direct");
__m128 *SseIn1 = (__m128*)IN1;
__m128 *SseIn2 = (__m128*)IN2;
__m128 *SseOut = (__m128*)OUT2;
for (unsigned i = 0; i < COUNT; i++)
{
*SseOut = _mm_add_ps(*SseIn1, *SseIn2);
SseIn1++;
SseIn2++;
SseOut++;
}
}
I mi to zadziałało. Wersja "Direct" zadziałała szybciej, niż "Load+Store".

Trzeba tylko pilnować tego wrednego wyrównania, bo bez align(16) potrafi się wysypywać (np. tylko w Release, a w Debug nie).
Zapisane

Esidar
SuperHero Member
******

wiadomości: 1359


Zobacz profil
« Odpowiedz #33 : Grudzień 02, 2008, 23:39:08 »

W wersji Release nie powinno mieć znaczenia która wersja. Obydwie są równoważne. Jedyna różnica jest taka, że w drugiej zwiększasz 3 wskaźniki a w pierwszej 'i' jest mnożone 3 razy. Nie wiem co z tego kompilator wygeneruje Smiley
Zapisane
Sebastian
Newbie
*

wiadomości: 34


Zobacz profil
« Odpowiedz #34 : Grudzień 03, 2008, 01:30:37 »

Cytuj
Ale na starszych SSE trzeba radzić sobie tak jak powyżej

W SSE3 jest instrukcja HADD , która ułatwia implementacje DOTa , szczególnie kiedy wykonujemy operacje na 4 parach wektorów.
Zapisane
mINA87
SuperHero Member
******

wiadomości: 1145


Zobacz profil
« Odpowiedz #35 : Grudzień 03, 2008, 01:38:31 »

Trzeba tylko pilnować tego wrednego wyrównania, bo bez align(16) potrafi się wysypywać.
Są przecież wersje unaligned Smiley
Zapisane

"2nd hand car, 2nd hand shoes, 2nd hand point of view from the 2nd hand news.
2nd hand shirt, 2nd hand tie, 2nd hand reason from a 2nd hand lie. (...)"
Pitchshifter - 2nd hand
Khaine
Full Member
***

wiadomości: 205


Zobacz profil
« Odpowiedz #36 : Grudzień 16, 2009, 19:31:15 »

Postanowilem odkopac ten temat, gdyz postanowilem ostatnio zaimplementowac troche matematyki z uzyciem SSE i okazalo sie, ze mam kilka pytan:

1. W czym jest lepsze uzycie funkcji _mm_store_ss od r.m128_f32[0] (gdzie r jest typu __m128)?

2. Mamy mniej wiecej taki (pseudo)kod:
Kod:
#define ALIGN(_bytes) __declspec(align(_bytes))
ALIGN(16) class Vector3
{
    public:

        union
{
    ALIGN(16) struct
    {
        f32 x;
        f32 y;
        f32 z;
        f32 w;
    };

   ALIGN(16) __m128 xyzw;
};

typedef ALIGN(16) Vector3 Vec3;

__forceinline Vec3 MulRef(const Vec3& _vecLeft, const Vec3& _vecRight)
{
return Vec3(_vecLeft.x * _vecRight.x, _vecLeft.y * _vecRight.y, _vecLeft.z * _vecRight.z);
}

__forceinline Vec3 MulSSE2(const Vec3& _vecLeft, const Vec3& _vecRight)
{
        //Vec3(const __m128& m) : xyzw(m) {}
return _mm_mul_ps(_vecLeft.xyzw, _vecRight.xyzw);
}

const unsigned int ulCount = 10000;

Vec3 v1[ulCount];
Vec3 v2[ulCount];
Vec3 vR[3][ulCount];

{
CSimpleProfiler Prof("ref");
for(u32 i = 0; i < ulCount; ++i)
{
vR[[0][i] = MulRef(v1[i], v2[i]);
}

}

{
CSimpleProfiler Prof("sse2");
for(u32 i = 0; i < ulCount; ++i)
{
vR[1][i] = MulSSE2(v1[i], v2[i]);
}
}

{
CSimpleProfiler Prof("sse2a");
for(u32 i = 0; i < ulCount; ++i)
{
vR[2][i] = _mm_mul_ps(v1[i].xyzw, v2[i].xyzw);
}
}


Oto czasy wykonania (milisekundy) w wersji debug (intel i5 2.66, podobne czasy na intel i7 920 2.66):
ref :    0.0028
sse2 :  0.0029
sse2a : 0.0020

dla funkcji dot:
ref: :    0.0026
sse2: :  0.0031
sse2a: : 0.0024

dla funkcji length:
ref: :    0.0035
sse2: :  0.0033
sse2a: : 0.0025

dla funkcji sqrt (pierwiastek z kazdej skladowej wektora), wersja ref liczy 3 pierwiastki
ref: :    0.0051
sse2: :  0.0028
sse2a: : 0.0020

dot w sse2 sciagnalem od Esidara.

W wersji release jest jeszcze gorzej jesli chodzi o wydajnosc sse. Oto srednie czasy dla mnozenia wektorow (milisekundy):
ref :    0.0000838
sse2 :  0.000161
sse2a : 0.0000907

oraz dot:
ref: :   0.0000838
sse2: : 0.000122
sse2a : 0.0000930
sse4 :  0.0000922
sse4a : 0.0000907

Gdybym napisal taki kod jak Reg, to wyszlo by duzo na kozysc sse, natomiast ja uzywam wskaznikow na funkcje, w zaleznosci od supportowanego sse, wiec opakowanie musi byc. Czy moglby mi ktos powiedziec czy tak powinno byc, czy moze skopalem kod?

Okazuje sie, ze wersja sse2 jest nieco wolniejsza od wersji 'normalnej', rzadko bywa szybsza. Natomiast, co mnie troche dziwi, zdecydowanie najszybsza jest wersja sse2a, czyli bezposrednie wywolanie funkcji sse. Czy opakowanie uniemozliwialo kompilatorowi optymalizacje? A moze samo wywolanie funkcji jest na tyle kosztowne? W niektorych przypadkach zdarza sie, ze wersja 'sse2' jest naprawde duzo wolniejsza niz 'sse2a', wiec mysle, ze to nie moze byc spowodowane narzutem dodatkowej funkcji. Czy moglby mi ktos jasno wylozyc o co chodzi?

W praktyce tylko pierwiastkowanie (wektora) daje rade byc szybsze. Pierwiastkowanie dla pojedynczej liczby (opakowane) rzadko bywa szybsze. Czasami dot w sse4 byl duzo szybszy - ~0.00004ms w release, ale w wiekszosci przypadkow wypadal gorzej niz wersja 'ref'.
Zapisane
.c41x
Jr. Member
**

wiadomości: 94



Zobacz profil WWW
« Odpowiedz #37 : Grudzień 16, 2009, 20:53:25 »

najlepiej sprawdz sobie jaki kod (Assembly) generuje kazda wersja, to duzo wyjasni.
Zapisane
Khaine
Full Member
***

wiadomości: 205


Zobacz profil
« Odpowiedz #38 : Grudzień 16, 2009, 22:50:28 »

ano sprawdzalem, zreszta posrednio o tym pisalem. Wydaje mi sie, ze najwydajniejszy kod assemblera to ten najkrotszy (pomijajac ilosc cykli na operacje) (nie jestem wyjadaczem asm, wiec moge mowic herezje), a dla "normalnej" wersji kazdej z funkcji, ktora uzywalem, kod assemblera byl zdecydowanie najkrotszy. Dla sse w przypadku iloczunu skalarnego kod asm jest ok. 3 razy dluzszy.

Mniej wiecej tak wyglada dot (debug):
Kod:
xst_fi f32 DotRef(const Vec3& _vecLeft, const Vec3& _vecRight)
{
/*
        kod wygenerowany dla ponizszej linijki, bez return
0135FB2E  mov         eax,dword ptr [_vecLeft]
0135FB31  fld         dword ptr [eax]
0135FB33  mov         ecx,dword ptr [_vecRight]
0135FB36  fmul        dword ptr [ecx]
0135FB38  mov         edx,dword ptr [_vecLeft]
0135FB3B  fld         dword ptr [edx+4]
0135FB3E  mov         eax,dword ptr [_vecRight]
0135FB41  fmul        dword ptr [eax+4]
0135FB44  faddp       st(1),st
0135FB46  mov         ecx,dword ptr [_vecLeft]
0135FB49  fld         dword ptr [ecx+8]
0135FB4C  mov         edx,dword ptr [_vecRight]
0135FB4F  fmul        dword ptr [edx+8]
0135FB52  faddp       st(1),st
0135FB54  fstp        dword ptr [ebp-0C4h]
0135FB5A  fld         dword ptr [ebp-0C4h]
*/
      return _vecLeft.x * _vecRight.x + _vecLeft.y * _vecRight.y + _vecLeft.z * _vecRight.z;
}

i dla sse
Kod:
xst_i f32 DotSSE2(const Vec3& _vecLeft, const Vec3& _vecRight)
{
/*
013673AA  mov         eax,dword ptr [ebx+0Ch]
013673AD  movaps      xmm0,xmmword ptr [eax]
013673B0  mov         ecx,dword ptr [ebx+8]
013673B3  movaps      xmm1,xmmword ptr [ecx]
013673B6  mulps       xmm1,xmm0
013673B9  movaps      xmmword ptr [ebp-180h],xmm1
013673C0  movaps      xmm0,xmmword ptr [ebp-180h]
013673C7  movaps      xmmword ptr [ebp-20h],xmm0
*/
m128 r1 = _mm_mul_ps( _vecLeft.xyzw , _vecRight.xyzw );
/*
013673CB  movaps      xmm0,xmmword ptr [ebp-20h]
013673CF  movaps      xmm1,xmmword ptr [ebp-20h]
013673D3  movhlps     xmm1,xmm0
013673D6  movaps      xmmword ptr [ebp-160h],xmm1
013673DD  movaps      xmm0,xmmword ptr [ebp-160h]
013673E4  movaps      xmm1,xmmword ptr [ebp-20h]
013673E8  addps       xmm1,xmm0
013673EB  movaps      xmmword ptr [ebp-140h],xmm1
013673F2  movaps      xmm0,xmmword ptr [ebp-140h]
013673F9  movaps      xmmword ptr [ebp-20h],xmm0
*/
r1 = _mm_add_ps( r1, _mm_movehl_ps(r1, r1));
/*
013673FD  movaps      xmm0,xmmword ptr [ebp-20h]
01367401  movaps      xmm1,xmmword ptr [ebp-20h]
01367405  shufps      xmm1,xmm0,0E5h
01367409  movaps      xmmword ptr [ebp-120h],xmm1
01367410  movaps      xmm0,xmmword ptr [ebp-120h]
01367417  movaps      xmm1,xmmword ptr [ebp-20h]
0136741B  addps       xmm1,xmm0
0136741E  movaps      xmmword ptr [ebp-100h],xmm1
01367425  movaps      xmm0,xmmword ptr [ebp-100h]
0136742C  movaps      xmmword ptr [ebp-20h],xmm0
*/
r1 = _mm_add_ps( r1, _mm_shuffle_ps(r1, r1, _MM_SHUFFLE(3, 2, 1, 1)));
return r1.m128_f32[0];
}
Duzo mov'a, ktory przeciez nie jest darmowy.
Zapisane
yarpen
SuperHero Member
******

wiadomości: 1423


Zobacz profil WWW
« Odpowiedz #39 : Grudzień 16, 2009, 22:57:21 »

Olej wersje Debug, pokaz Release.
[Edit] A zreszta, bez znaczenia. SSE nie da Ci wielkiej roznicy na operacjach typu dot produkt na 2 wektorach. Dopalenie widac dopiero gdy sie operuje na blokach danych.
« Ostatnia zmiana: Grudzień 16, 2009, 22:59:29 wysłane przez yarpen » Zapisane
.c41x
Jr. Member
**

wiadomości: 94



Zobacz profil WWW
« Odpowiedz #40 : Grudzień 16, 2009, 23:43:14 »

na FPU jest oczywiscie dluzszy, mozesz napisac sobie tez to jako wstawke (co wg. mnie jest duzo czytelniejsze od visualowego zapisu), pewnie bedzie szybciej i bedzie mniej 'mov-ow'. przyspieszenie czuc jak wpomnial kolega wyzej przy duzych porcjach danych np. operacje na macierzach, kolizje, przetwarzanie obrazow (to dla MMX i SSE2+).
Zapisane
msieradzki
Jr. Member
**

wiadomości: 52


Zobacz profil
« Odpowiedz #41 : Grudzień 18, 2009, 03:12:16 »

Co obsługuje SSE? SSE2 jest w każdym x86_64 i w ~98% komputerów używanych do gier (p. Steam Hardware Survey). Nie wymieniają niestety SSE3 i wyższych.

SSE wymaga zmiany organizacji kodu. Zamiast myśleć o opóźnieniu trzeba nastawić się na przepustowość. Możesz napisać kod śledzący promienie i będzie działał z prędkością 1x na x87, 1,5x po zoptymalizowaniu z SSE, 4x po reorganizacji kodu na śledzenie 4 promieni naraz. Źródło: http://www.sci.utah.edu/~wald/PhD/wald_phd.pdf Mogłem odrobinę pomylić liczby.

Intel w swoich dokumentach o SSE też mówi o zmianie z AoS na SoA itp.
Zapisane
Khaine
Full Member
***

wiadomości: 205


Zobacz profil
« Odpowiedz #42 : Grudzień 18, 2009, 03:45:37 »

no bo na tym polega streaming. Tak samo jak w gpgpu
Zapisane
Strony: 1 2 [3]
  Drukuj  
 
Skocz do:  

Hosting: Polska Strefa - Ogłoszenia
Powered by SMF 1.1.7 | SMF © 2006, Simple Machines LLC