2013-02-19 10 views
15

Próbuję znaleźć najbardziej efektywną implementację mnożenia macierzy 4x4 (M) za pomocą wektora (u) za pomocą SSE. Chodzi mi o to Mu = vEfektywne mnożenie wektorów macierzy 4x4 za pomocą SSE: poziomy produkt dodany i kropkowany - o co w tym chodzi?

O ile rozumiem, istnieją dwa podstawowe sposoby, aby przejść na ten temat.

method 1) v1 = dot(row1, u), v2 = dot(row2, u), v3 = dot(row3, u), v4 = dot(row4, u) 
    method 2) v = u1 col1 + u2 col2 + u3 col3 + u4 col4. 

Metoda 2 jest łatwa do wdrożenia w SSE2. Metoda 1 może być implementowana za pomocą poziomej instrukcji dodawania w SSE3 lub instrukcji produktu dot w SSE4. Jednak we wszystkich moich testach metoda 2 zawsze przewyższa metodę 1.

Jednym miejscem, w którym chociaż metoda 1 miałaby przewagę, jest macierz 3x4, na przykład dla transformacji afinicznej. W tym przypadku ostatni produkt z kropką jest niepotrzebny. Ale nawet w tym przypadku metoda 2 na macierzy 4x4 jest szybsza niż metoda 1 na macierzy 3x4. Jedyną metodą, którą odkryłem, że jest szybsza niż metoda 2 na macierzy 4x4, jest metoda 2 na macierzy 4x3.

Jaki jest więc cel instrukcji dodawania poziomego i instrukcji dot? W rzeczywistości instrukcja wytwarzania kropek daje najgorszą wydajność w tym przypadku. Może ma to coś wspólnego z formatem danych? Jeśli nie można zdefiniować, w jaki sposób uporządkowana jest macierz, konieczna jest jej transpozycja iw takim przypadku może metoda 1 byłaby lepsza?

Zobacz poniżej, aby uzyskać kod.

__m128 m4x4v_colSSE(const __m128 cols[4], const __m128 v) { 
    __m128 u1 = _mm_shuffle_ps(v,v, _MM_SHUFFLE(0,0,0,0)); 
    __m128 u2 = _mm_shuffle_ps(v,v, _MM_SHUFFLE(1,1,1,1)); 
    __m128 u3 = _mm_shuffle_ps(v,v, _MM_SHUFFLE(2,2,2,2)); 
    __m128 u4 = _mm_shuffle_ps(v,v, _MM_SHUFFLE(3,3,3,3)); 

    __m128 prod1 = _mm_mul_ps(u1, cols[0]); 
    __m128 prod2 = _mm_mul_ps(u2, cols[1]); 
    __m128 prod3 = _mm_mul_ps(u3, cols[2]); 
    __m128 prod4 = _mm_mul_ps(u4, cols[3]); 

    return _mm_add_ps(_mm_add_ps(prod1, prod2), _mm_add_ps(prod3, prod4)); 
} 

__m128 m4x4v_rowSSE3(const __m128 rows[4], const __m128 v) { 
    __m128 prod1 = _mm_mul_ps(rows[0], v); 
    __m128 prod2 = _mm_mul_ps(rows[1], v); 
    __m128 prod3 = _mm_mul_ps(rows[2], v); 
    __m128 prod4 = _mm_mul_ps(rows[3], v); 

    return _mm_hadd_ps(_mm_hadd_ps(prod1, prod2), _mm_hadd_ps(prod3, prod4)); 
} 

__m128 m4x4v_rowSSE4(const __m128 rows[4], const __m128 v) { 
    __m128 prod1 = _mm_dp_ps (rows[0], v, 0xFF); 
    __m128 prod2 = _mm_dp_ps (rows[1], v, 0xFF); 
    __m128 prod3 = _mm_dp_ps (rows[2], v, 0xFF); 
    __m128 prod4 = _mm_dp_ps (rows[3], v, 0xFF); 

    return _mm_shuffle_ps(_mm_movelh_ps(prod1, prod2), _mm_movelh_ps(prod3, prod4), _MM_SHUFFLE(2, 0, 2, 0)); 
} 

Odpowiedz

10

horizontal Dodaj produkt kropka i instrukcje są złożone: są one rozkładane na wiele prostszych mikrooperacji które są wykonywane przez procesor, podobnie jak prostymi instrukcjami. Dokładna dekompozycja poziomych instrukcji dodawania i kropek do mikrooperacji jest specyficzna dla procesora, ale w przypadku najnowszych procesorów Intela, poziome dodawanie jest rozkładane na 2 mikrooperacje SHUFFLE + 1 ADD, a produkt kropki jest rozkładany na 1 MUL + 1 SHUFFLE + 2 ADD microoperations. Poza większą liczbą mikrooperacji, instrukcja ta również kładzie nacisk na dekoder instrukcji w potoku procesora: procesory Intela mogą dekodować tylko jedną tak złożoną instrukcję na cykl (w porównaniu do 4 prostych instrukcji). W przypadku buldożera AMD względny koszt tych złożonych instrukcji jest nawet wyższy.

+0

Dzięki, to wyjaśnia, dlaczego instrukcje są wolne. Nie wyjaśnia jednak, dlaczego zostały one wdrożone. Ale myślę, że teraz wiem. Metoda 2 wymaga, aby dane były strukturą tablic (SoA), tj. Uporządkowaną kolumną, aby były optymalne. Jeśli dane są tablicą struktur (AoS), tj. Uporządkowanym rzędem, należy wykonać transpozycję iw tym przypadku metoda 1 jest znacznie szybsza. Innymi słowy, jeśli dane można zdefiniować, należy utworzyć SoA zamiast AoS i użyć metody 2. W przeciwnym razie należy użyć metody 1 z dodaniem poziomym. Nie używaj instrukcji tworzenia kropek do mnożenia macierzy. –

+1

Producenci procesorów mają historię dodawania nowych instrukcji, które mogą być bardzo przydatne, ale początkowo poświęcają bardzo mało sprzętu do ich implementacji. Jeśli zostaną zaadoptowani przez wystarczającą liczbę programów, ostatecznie dodadzą więcej sprzętu, aby przyspieszyć wykonanie instrukcji. Pierwsza generacja '' _mm_dp_ps'' nie jest tak naprawdę szybsza niż zwykłe podejście SSE lub SSE3 do tego, chociaż teoretycznie powinno być nieco mniej zarzucania kodu, jeśli robisz dużo z nich. –

+0

Jeśli spojrzysz na podręcznik Intrinsics Intela: [link] (https://software.intel.com/sites/landingpage/IntrinsicsGuide/#techs=SSE3,SSE4_1&cats=Arithmetic&expand=2737,2084), zobaczysz dane dotyczące wydajności. Powinno to również pomóc wyjaśnić, dlaczego rozwiązanie dp jest zdecydowanie lepsze od rozwiązania hadd. – St0fF

Powiązane problemy