Profilowałem część naszej podstawowej matematyki na Intel Core Duo i patrząc na różne podejścia do pierwiastka kwadratowego zauważyłem coś dziwnego: używając operacji skalarnych SSE, szybciej jest wziąć odwrotność pierwiastka kwadratowego i pomnożyć go aby uzyskać sqrt, niż użyć natywnego kodu operacji sqrt!
Testuję to z pętlą coś takiego:
inline float TestSqrtFunction( float in );
void TestFunc()
{
#define ARRAYSIZE 4096
#define NUMITERS 16386
float flIn[ ARRAYSIZE ]; // filled with random numbers ( 0 .. 2^22 )
float flOut [ ARRAYSIZE ]; // filled with 0 to force fetch into L1 cache
cyclecounter.Start();
for ( int i = 0 ; i < NUMITERS ; ++i )
for ( int j = 0 ; j < ARRAYSIZE ; ++j )
{
flOut[j] = TestSqrtFunction( flIn[j] );
// unrolling this loop makes no difference -- I tested it.
}
cyclecounter.Stop();
printf( "%d loops over %d floats took %.3f milliseconds",
NUMITERS, ARRAYSIZE, cyclecounter.Milliseconds() );
}
Próbowałem tego z kilkoma różnymi ciałami dla TestSqrtFunction i mam kilka czasów, które naprawdę drapią mnie po głowie. Zdecydowanie najgorsze było użycie natywnej funkcji sqrt () i pozwolenie „inteligentnemu” kompilatorowi na „optymalizację”. Przy 24ns / float, przy użyciu FPU x87 było to żałośnie złe:
inline float TestSqrtFunction( float in )
{ return sqrt(in); }
Następną rzeczą, jaką próbowałem, było użycie funkcji wewnętrznej, aby zmusić kompilator do użycia skalarnego kodu operacji sqrt SSE:
inline void SSESqrt( float * restrict pOut, float * restrict pIn )
{
_mm_store_ss( pOut, _mm_sqrt_ss( _mm_load_ss( pIn ) ) );
// compiles to movss, sqrtss, movss
}
To było lepsze, przy 11,9ns / float. Wypróbowałem również zwariowaną technikę przybliżenia Newtona-Raphsona Carmacka , która działała nawet lepiej niż sprzęt, przy 4,3 ns / float, chociaż z błędem 1 na 2 10 (co jest zbyt duże dla moich celów).
Doozy miał miejsce, gdy próbowałem opcją SSE dla odwrotności pierwiastka kwadratowego, a następnie użyłem mnożenia, aby uzyskać pierwiastek kwadratowy (x * 1 / √x = √x). Mimo to trwa dwie operacje zależne było najszybsze rozwiązanie zdecydowanie na 1.24ns / pływaka i dokładnością do 2 -14 :
inline void SSESqrt_Recip_Times_X( float * restrict pOut, float * restrict pIn )
{
__m128 in = _mm_load_ss( pIn );
_mm_store_ss( pOut, _mm_mul_ss( in, _mm_rsqrt_ss( in ) ) );
// compiles to movss, movaps, rsqrtss, mulss, movss
}
Moje pytanie brzmi: co daje ? Dlaczego kod operacji pierwiastka kwadratowego wbudowany w sprzęt SSE jest wolniejszy niż jego synteza z dwóch innych operacji matematycznych?
Jestem pewien, że to tak naprawdę koszt samej operacji, bo zweryfikowałem:
- Wszystkie dane mieszczą się w pamięci podręcznej, a dostęp jest sekwencyjny
- funkcje są wbudowane
- rozwijanie pętli nie robi różnicy
- flagi kompilatora są ustawione na pełną optymalizację (a montaż jest dobry, sprawdziłem)
( edytuj : stephentyrone poprawnie wskazuje, że operacje na długich ciągach liczb powinny wykorzystywać wektoryzację operacji spakowanych w SIMD, na przykład rsqrtps
- ale struktura danych tablicy jest tutaj tylko do celów testowych: to, co naprawdę próbuję zmierzyć, to wydajność skalarna do użycia w kodzie których nie można wektoryzować).
inline float SSESqrt( float restrict fIn ) { float fOut; _mm_store_ss( &fOut, _mm_sqrt_ss( _mm_load_ss( &fIn ) ) ); return fOut; }
. Jest to jednak zły pomysł, ponieważ może łatwo wywołać przeciągnięcie magazynu typu load-hit-store, jeśli procesor zapisuje liczby zmiennoprzecinkowe na stosie, a następnie odczytuje je natychmiast - żonglowanie z rejestru wektorowego do rejestru zmiennoprzecinkowego w szczególności dla wartości zwracanej to zła wiadomość. Poza tym podstawowe instrukcje maszynowe, które reprezentują elementy wewnętrzne SSE, i tak przyjmują operandy adresu.
eax
) jest bardzo złe, podczas gdy podróż w obie strony między xmm0 a stosem a z powrotem nie jest ze względu na przekazywanie do sklepu przez firmę Intel. Możesz to zrobić samemu, aby to sprawdzić. Generalnie najłatwiejszym sposobem zobaczenia potencjalnego LHS jest przyjrzenie się emitowanemu złożeniu i zobaczenie, gdzie dane są żonglowane między zestawami rejestrów; Twój kompilator może zrobić mądrą rzecz lub może nie. Co do normalizacji wektorów, moje wyniki spisałem