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.)
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ę.
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.
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 :).
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)
I: pełny krok aktualizacji new_x = x + dx
następnie brzmi:
x *= 3/2 - y/2 * x * x
co też jest łatwe.
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.
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)
.
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.
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.
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