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

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ąć wzajemny pierwiastek kwadratowy i pomnożyć go, aby uzyskać sqrt, niż używać natywnego kodu opcode sqrt!

Testuję z pętlą coś w stylu:

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 funkcji TestSqrtFunction, i mam kilka timingów to naprawdę drapie mnie po głowie. Najgorsze było użycie natywnej funkcji sqrt () i umożliwienie "inteligentnemu" kompilatorowi "optymalizacji". W 24ns / float, używając FPU x87 było to patetycznie złe:

inline float TestSqrtFunction( float in )
{  return sqrt(in); }

Następną rzeczą, którą próbowałem było użycie intrinsic, aby zmusić kompilator do użycia skalarnego kodu SSE sqrt:

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
}
Tak było lepiej, przy 11.9 ns / float. Spróbowałem też zwariowanej techniki przybliżenia Newtona-Raphsona, która działała nawet lepiej niż sprzęt, w 4.3 NS / float, chociaż z błędem 1 w 210 (czyli za dużo jak na moje cele).

Doozy był, kiedy próbowałem SSE op dla wzajemny pierwiastek kwadratowy, a następnie użył mnożnika, aby uzyskać pierwiastek kwadratowy (x * 1/√x = √x ). Mimo, że wymaga to dwóch zależnych operacji, było to zdecydowanie najszybsze rozwiązanie, z prędkością 1,24 ns / float 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 zasadniczo co daje ? dlaczego SSE jest wbudowany w sprzęt kod pierwiastka kwadratowego wolniejszy niż zsyntetyzowanie go z dwóch innych operacji matematycznych?

Jestem pewien, że to naprawdę koszt samego op, bo zweryfikowałem:

  • wszystkie dane mieszczą się w pamięci podręcznej, a dostęp jest sekwencyjny
  • funkcje są inlined
  • rozwijanie pętli nie robi różnicy
  • W tym samym czasie udało się stworzyć nową wersję kompilatora, która została zaprojektowana z myślą o optymalizacji kompilatora.]}

(edit : stephentyrone poprawnie zaznacza, że operacje na długich ciągach liczb powinny używać wektoryzującego SIMD spakowanego ops, jak rsqrtps - ale struktura danych tablicy tutaj jest tylko do celów testowych: to, co naprawdę próbuję zmierzyć, to wydajność Skalar do użycia w kodzie, który nie może być wektoryzowany.)

Author: It'sNotALie., 2009-10-06

6 answers

sqrtss daje prawidłowo zaokrąglony wynik. rsqrtss daje przybliżenie do odwrotności, dokładne do około 11 bitów.

sqrtss generuje znacznie dokładniejszy wynik, gdy wymagana jest dokładność. rsqrtss istnieje w przypadkach, gdy wystarczy przybliżenie, ale wymagana jest szybkość. Jeśli przeczytasz dokumentację Intela, znajdziesz również sekwencję instrukcji (wzajemne przybliżenie pierwiastka kwadratowego, po którym następuje pojedynczy krok Newtona-Raphsona), która daje niemal pełną precyzję (~23 bity dokładności, o ile dobrze pamiętam), i nadal jest nieco szybszy niż sqrtss.

Edit: jeśli szybkość jest krytyczna, a tak naprawdę wywołujesz ją w pętli dla wielu wartości, powinieneś używać wektoryzowanych wersji tych instrukcji, rsqrtps lub sqrtps, z których obie przetwarzają cztery pływaki na instrukcję.

 218
Author: Stephen Canon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-10-07 00:02:29

Dotyczy to również podziału. MULSS(a,RCPSS(b)) jest o wiele szybszy niż DIVSS (a,b). W rzeczywistości jest jeszcze szybszy, nawet jeśli zwiększysz jego precyzję iteracją Newtona-Raphsona.

Intel i AMD zalecają tę technikę w swoich podręcznikach optymalizacji. W aplikacjach, które nie wymagają zgodności ze STANDARDEM IEEE-754, jedynym powodem użycia div / sqrt jest czytelność kodu.

 8
Author: Spat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-25 10:46:27

Zamiast podawać odpowiedź, która faktycznie może być niepoprawna (Nie będę też sprawdzać ani dyskutować o cache i innych rzeczach, powiedzmy, że są identyczne) postaram się wskazać źródło, które może odpowiedzieć na twoje pytanie.
Różnica może leżeć w tym, jak sqrt i rsqrt są obliczane. Możesz przeczytać więcej tutaj http://www.intel.com/products/processor/manuals / . proponuję zacząć od czytania o funkcjach procesora, których używasz, są pewne informacje, zwłaszcza o rsqrt (cpu używa wewnętrznej tabeli wyszukiwania z dużym przybliżeniem, co znacznie ułatwia uzyskanie wyniku). Może się wydawać, że rsqrt jest o tyle szybszy od sqrt, że 1 dodatkowa operacja wielostopniowa (która nie jest zbyt kosztowna) może nie zmienić sytuacji.

Edit: kilka faktów, o których warto wspomnieć:
1. Kiedyś robiłem mikro optymalizacje dla mojej biblioteki graficznej i używałem rsqrt do obliczania długości wektorów. (zamiast sqrt pomnożyłem swoje suma do kwadratu przez rsqrt, czyli dokładnie to, co zrobiłeś w swoich testach), i wypadło lepiej.
2. Obliczanie rsqrt przy użyciu prostej tabeli wyszukiwania może być łatwiejsze, jak dla rsqrt, gdy x idzie do nieskończoności, 1 / sqrt(x) idzie do 0, więc dla małych X wartości funkcji nie zmieniają się (dużo), podczas gdy dla sqrt - idzie do nieskończoności, więc to jest ten prosty przypadek ;).

Również wyjaśnienie: nie jestem pewien, gdzie znalazłem to w książkach, które linkowałem, ale jestem prawie pewien, że czytałem, że rsqrt jest używając jakiejś tabeli lookup, i powinna być używana tylko wtedy, gdy wynik nie musi być dokładny , chociaż-mogę się mylić, jak to było jakiś czas temu :).

 6
Author: Marcin Deptuła,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-10-07 00:14:18

Newton-Raphson jest zbieżny do zera z f(x) używając przyrostów równych -f/f' Gdzie f' jest pochodną.

Dla x=sqrt(y), Możesz spróbować rozwiązać f(x) = 0 dla x używając f(x) = x^2 - y;

Wtedy przyrost wynosi: dx = -f/f' = 1/2 (x - y/x) = 1/2 (x^2 - y) / x która ma w sobie powolny podział.

Możesz wypróbować inne funkcje (jak f(x) = 1/y - 1/x^2), ale będą one równie skomplikowane.

Spójrzmy na 1/sqrt(y) Teraz. Możesz spróbować f(x) = x^2 - 1/y, ale będzie to równie skomplikowane: dx = 2xy / (y*x^2 - 1) na przykład. Jedna nieoczywista alternatywa wybór dla f(x) to: f(x) = y - 1/x^2

Potem: dx = -f/f' = (y - 1/x^2) / (2/x^3) = 1/2 * x * (1 - y * x^2)

Ah! To nie jest trywialne wyrażenie, ale masz w nim tylko mnożenia, nie ma podziału. = > Szybciej!

I: pełny krok aktualizacji new_x = x + dx następnie brzmi:

x *= 3/2 - y/2 * x * x co też jest łatwe.

 4
Author: skal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-05 22:08:22

Istnieje wiele innych odpowiedzi na to już od kilku lat. Oto co konsensus ma rację:

  • instrukcje rsqrt * obliczają przybliżenie do wzajemnego pierwiastka kwadratowego, dobre do około 11-12 bitów.
  • jest zaimplementowany z tabelą wyszukiwania (np. ROM) indeksowaną przez mantysę. (W rzeczywistości jest to skompresowana tabela wyszukiwania, podobna do starych tabel matematycznych, wykorzystująca korekty bitów niskiego rzędu, aby zapisać na tranzystorach.)
  • The powodem, dla którego jest on dostępny, jest to, że jest to wstępne oszacowanie używane przez FPU dla" rzeczywistego " algorytmu pierwiastka kwadratowego.
  • jest też przybliżona Instrukcja wzajemna, rcp. Obie te instrukcje są wskazówką jak FPU implementuje pierwiastek kwadratowy i podział.

Oto co konsensus się pomylił:

  • SSE-era FPU nie używa Newtona-Raphsona do obliczania pierwiastków kwadratowych. Jest to świetna metoda w oprogramowaniu, ale błędem byłoby zaimplementować ją w ten sposób w sprzęt.
Algorytm N-R do obliczania wzajemnego pierwiastka kwadratowego ma ten krok aktualizacji, jak zauważyli inni:]}
x' = 0.5 * x * (3 - n*x*x);

To dużo mnożenia zależnego od danych i jedno odejmowanie.

Poniżej znajduje się algorytm, którego współczesne FPU faktycznie używają.

Biorąc pod uwagę b[0] = n, Załóżmy, że możemy znaleźć szereg liczb Y[i] takich, że b[n] = b[0] * Y[0]^2 * Y[1]^2 * ... * Y[n]^2 zbliża się do 1. Następnie rozważ:

x[n] = b[0] * Y[0] * Y[1] * ... * Y[n]
y[n] = Y[0] * Y[1] * ... * Y[n]

Wyraźnie x[n] podejść sqrt(n) i y[n] podejść 1/sqrt(n).

Możemy użyć kroku aktualizacji Newtona-Raphsona dla wzajemnego pierwiastka kwadratowego, aby uzyskać dobry Y[i]:
b[i] = b[i-1] * Y[i-1]^2
Y[i] = 0.5 * (3 - b[i])

Potem:

x[0] = n Y[0]
x[i] = x[i-1] * Y[i]

I:

y[0] = Y[0]
y[i] = y[i-1] * Y[i]

Kolejną kluczową obserwacją jest to, że b[i] = x[i-1] * y[i-1]. Więc:

Y[i] = 0.5 * (3 - x[i-1] * y[i-1])
     = 1 + 0.5 * (1 - x[i-1] * y[i-1])

Potem:

x[i] = x[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
     = x[i-1] + x[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))
y[i] = y[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
     = y[i-1] + y[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))

To znaczy, biorąc pod uwagę początkowe x i y, możemy użyć następującego kroku aktualizacji:

r = 0.5 * (1 - x * y)
x' = x + x * r
y' = y + y * r

Lub, nawet bardziej fantazyjne, możemy ustawić h = 0.5 * y. Jest to inicjalizacja:

Y = approx_rsqrt(n)
x = Y * n
h = Y * 0.5

A to jest aktualizacja krok:

r = 0.5 - x * h
x' = x + x * r
h' = h + h * r

Jest to algorytm Goldschmidta, który ma ogromną zaletę, jeśli implementujesz go w sprzęcie: "wewnętrzna pętla" to trzy mnożniki i nic więcej, a dwa z nich są niezależne i mogą być pipelinowane.

W 1999 r.FPU już potrzebowało pipelined obwodu dodawania/odejmowania i Pipelined obwodu mnożenia, w przeciwnym razie SSE nie byłoby zbyt "strumieniowe". Tylko jeden z każdego obwodu był potrzebny w 1999 roku, aby wdrożyć tę wewnętrzną pętlę w pełni pipelined sposób bez marnowanie dużo sprzętu tylko na pierwiastek kwadratowy.

Dzisiaj, oczywiście, połączyliśmy multiply-add z programistą. Ponownie, wewnętrzna pętla to trzy pipelinowe FMA, które są (ponownie) ogólnie użyteczne, nawet jeśli nie obliczasz pierwiastków kwadratowych.

 2
Author: Pseudonym,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-12-05 01:08:40

Jest to szybsze, ponieważ te instrukcje ignorują tryby zaokrąglania i nie obsługują WYJĄTKÓW punktów pływających lub liczb dernormalizowanych. Z tych powodów o wiele łatwiej jest rurociąg, spekulować i wykonywać inne instrukcje fp Nie w porządku.

 -3
Author: Witek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-05 14:17:12