Dlaczego skalarny SSE sqrt (x) jest wolniejszy niż rsqrt (x) * x?

Profilowałem część naszej podstawowej matematyki w 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ąć odwrotny pierwiastek kwadratowy i pomnożyć go aby pobrać sqrt, niż użyć natywnego kodu sqrt!

Testuję go za pomocą pętli:

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 taktów, które naprawdę drapią mnie po głowie. Najgorsze ze wszystkich było użycie rodzimej funkcji sqrt () i pozwolenie „inteligentnemu” kompilatorowi „zoptymalizować”. 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ą, którą próbowałem, było użycie funkcji wewnętrznej, aby zmusić kompilator do użycia skalarnego kodu sqrt w 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. Próbowałem teżZwariowana technika przybliżania Newtona-Raphsona, który działał nawet lepiej niż sprzęt, na 4.3ns / float, chociaż z błędem 1 na 210 (co jest zbyt wiele dla moich celów).

Doozy był, kiedy próbowałem SSE opodwrotność pierwiastek kwadratowy, a następnie użył mnożenia, aby uzyskać pierwiastek kwadratowy (x * 1 / √x = √x). Nawet jeśli wymaga to dwóch operacji zależnych, to było to najszybsze rozwiązanie na poziomie 1.24ns / float i dokładne na 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 jest w zasadzieco daje? Dlaczego wbudowany w sprzęt SSE opcode pierwiastka kwadratowegowolniej niż syntetyzowanie go z dwóch innych operacji matematycznych?

Jestem pewien, że jest to naprawdę koszt samego op, ponieważ sprawdziłem:

Wszystkie dane mieszczą się w pamięci podręcznej, a dostęp jest sekwencyjnyfunkcje są zaznaczonerozwijanie pętli nie ma znaczeniaflagi kompilatora są ustawione na pełną optymalizację (a montaż jest dobry, sprawdziłem)

(edytować: stephentyrone poprawnie wskazuje, że operacje na długich łańcuchach liczb powinny używać wektorowania SIMD zapakowanego w opsrsqrtps - ale struktura danych tablicy jest tutaj tylko dla celów testowych: to, co naprawdę staram się zmierzyć, toskalarny wydajność do wykorzystania w kodzie, którego nie można wektoryzować.)

questionAnswers(5)

yourAnswerToTheQuestion