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));
}
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. –
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. –
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