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ć).
źródło
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łemOdpowiedzi:
sqrtss
daje poprawnie zaokrąglony wynik.rsqrtss
podaje przybliżenie odwrotności, z dokładnością do około 11 bitów.sqrtss
generuje znacznie dokładniejsze wyniki, gdy wymagana jest dokładność.rsqrtss
istnieje w przypadkach, gdy wystarczy przybliżenie, ale wymagana jest prędkość. Jeśli przeczytasz dokumentację Intela, znajdziesz również sekwencję instrukcji (odwrotne przybliżenie pierwiastka kwadratowego, po którym następuje pojedynczy krok Newtona-Raphsona), która zapewnia prawie pełną precyzję (~ 23 bity dokładności, jeśli dobrze pamiętam) i nadal jest nieco szybciej niżsqrtss
.edycja: Jeśli szybkość ma kluczowe znaczenie i naprawdę wywołujesz to w pętli dla wielu wartości, powinieneś używać wektoryzowanych wersji tych instrukcji
rsqrtps
lubsqrtps
obie przetwarzają cztery zmiennoprzecinkowe na instrukcję.źródło
sqrtss
jest poprawnie zaokrąglony , co wymaga ~ 50 bitów przed zaokrągleniem i nie można go osiągnąć za pomocą prostej iteracji N / R z pojedynczą precyzją.Dotyczy to również podziału. MULSS (a, RCPSS (b)) jest znacznie szybszy niż DIVSS (a, b). W rzeczywistości jest nadal szybszy, nawet jeśli zwiększysz jego precyzję za pomocą iteracji 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żywania div / sqrt jest czytelność kodu.
źródło
div
nie jest to jedyna operacja, więc łączna przepustowość UOP jest często wąskim gardłem, nawet jeśli występujedivps
lubdivss
. Zobacz dzielenie zmiennoprzecinkowe a mnożenie zmiennoprzecinkowe , gdzie moja odpowiedź zawiera sekcję wyjaśniającą, dlaczegorcpps
nie jest już wygrana w przepustowości. (Lub wygrana z opóźnieniem) i liczby dotyczące dzielenia przepustowości / opóźnienia.a * rcpss(b)
może być szybsze, ale nadal jest to więcej niż upsa/b
!Zamiast udzielać odpowiedzi, która w rzeczywistości może być niepoprawna (nie zamierzam też sprawdzać ani dyskutować o pamięci podręcznej i innych rzeczach, powiedzmy, że są identyczne) spróbuję wskazać źródło, które może odpowiedzieć na twoje pytanie.
Różnica może polegać na sposobie obliczania sqrt i rsqrt. Więcej informacji można znaleźć tutaj http://www.intel.com/products/processor/manuals/ . Proponuję zacząć od przeczytania o funkcjach procesora, których używasz, jest trochę informacji, szczególnie o rsqrt (procesor używa wewnętrznej tabeli przeglądowej z ogromnym przybliżeniem, co znacznie ułatwia uzyskanie wyniku). Może się wydawać, że rsqrt jest o wiele szybszy niż sqrt, że 1 dodatkowa operacja mul (co nie jest zbyt kosztowna) może nie zmienić sytuacji tutaj.
Edycja: Kilka faktów, o których warto wspomnieć:
1. Kiedyś robiłem mikro optymalizacje dla mojej biblioteki graficznej i użyłem rsqrt do obliczania długości wektorów. (zamiast sqrt pomnożyłem sumę kwadratu przez rsqrt, co jest dokładnie tym, co zrobiłeś w swoich testach) i wypadło lepiej.
2. Obliczenie rsqrt przy użyciu prostej tabeli przeglądowej może być łatwiejsze, ponieważ dla rsqrt, gdy x dochodzi do nieskończoności, 1 / sqrt (x) idzie do 0, więc dla małych x wartości funkcji się nie zmieniają (dużo), podczas gdy dla sqrt - dąży do nieskończoności, więc to taki prosty przypadek;).
Ponadto wyjaśnienie: nie jestem pewien, gdzie znalazłem to w książkach, do których linkowałem, ale jestem prawie pewien, że czytałem, że rsqrt używa jakiejś tabeli odnośników i powinno być używane tylko wtedy, gdy wynik nie musi być dokładne, chociaż - ja też mogę się mylić, tak jak to było jakiś czas temu :).
źródło
Newton-Raphson zbiega się do zera
f(x)
przy użyciu przyrostów równych-f/f'
gdzief'
jest pochodną.Ponieważ
x=sqrt(y)
możesz spróbować rozwiązaćf(x) = 0
zax
pomocąf(x) = x^2 - y
;Wtedy przyrost jest następujący:
dx = -f/f' = 1/2 (x - y/x) = 1/2 (x^2 - y) / x
który ma powolny podział.Możesz wypróbować inne funkcje (np.
f(x) = 1/y - 1/x^2
), Ale będą one równie skomplikowane.Spójrzmy
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. Jednym z nieoczywistych alternatywnych opcjif(x)
jest:f(x) = y - 1/x^2
Następnie:
dx = -f/f' = (y - 1/x^2) / (2/x^3) = 1/2 * x * (1 - y * x^2)
Ach! To nie jest trywialne wyrażenie, ale masz w nim tylko mnożenia, bez dzielenia. => Szybciej!
I: pełny krok aktualizacji
new_x = x + dx
brzmi:x *= 3/2 - y/2 * x * x
co też jest łatwe.źródło
Istnieje wiele innych odpowiedzi na to pytanie sprzed kilku lat. Oto, co jest słuszne w konsensusie:
Oto dlaczego konsensus się nie zgadzał:
Algorytm NR do obliczania odwrotności pierwiastka kwadratowego ma ten krok aktualizacji, jak zauważyli inni:
To dużo mnożenia zależnego od danych i jedno odejmowanie.
Poniżej znajduje się algorytm, którego faktycznie używają nowoczesne jednostki FPU.
Biorąc pod uwagę
b[0] = n
, załóżmy, że możemy znaleźć szereg liczbY[i]
, któreb[n] = b[0] * Y[0]^2 * Y[1]^2 * ... * Y[n]^2
zbliżają się do 1. Następnie rozważ:Jasne
x[n]
podejściesqrt(n)
iy[n]
podejście1/sqrt(n)
.Możemy użyć kroku aktualizacji Newtona-Raphsona do odwrotności pierwiastka kwadratowego, aby uzyskać dobry
Y[i]
:Następnie:
i:
Następna kluczowa obserwacja jest taka
b[i] = x[i-1] * y[i-1]
. Więc:Następnie:
Oznacza to, że mając początkowe x i y, możemy użyć następującego kroku aktualizacji:
Lub, nawet bardziej wyszukane, możemy ustawić
h = 0.5 * y
. To jest inicjalizacja:A to jest krok aktualizacji:
To jest algorytm Goldschmidta, który ma ogromną zaletę, jeśli implementujesz go w sprzęcie: „pętla wewnętrzna” to trzy wielokrotne dodawanie i nic więcej, a dwa z nich są niezależne i można je potokować.
W 1999 r. Jednostki FPU potrzebowały już potokowego obwodu dodawania / odejmowania i potokowego obwodu wielokrotnego, w przeciwnym razie SSE nie byłoby zbyt „strumieniowe”. W 1999 r. Potrzebny był tylko jeden z każdego obwodu, aby zaimplementować tę wewnętrzną pętlę w sposób w pełni potokowy bez marnowania dużej ilości sprzętu tylko na pierwiastek kwadratowy.
Dziś oczywiście połączyliśmy mnożenie i dodawanie ujawnione programiście. Ponownie, pętla wewnętrzna to trzy potokowe FMA, które są (znowu) ogólnie przydatne, nawet jeśli nie obliczasz pierwiastków kwadratowych.
źródło
_mm256_rsqrt_ps
analizą perf Haswell. Zwykle jest to dobry pomysł tylko wtedy, gdy nie masz innej pracy w pętli i utrudniłoby to przepustowość rozdzielacza. HW sqrt jest pojedynczym uopem, więc można go mieszać z innymi pracami.Jest to szybsze, ponieważ te instrukcje ignorują tryby zaokrąglania i nie obsługują wyjątków zmiennoprzecinkowych ani zdernormalizowanych liczb. Z tych powodów znacznie łatwiej jest potokować, spekulować i wykonać inne instrukcje fp.
źródło
rsqrt
jest znacznie niższa dokładność, co oznacza znacznie mniej pracy do zrobienia (albo wcale?) Po stołowych odnośnika dostać przypuszczenie wyjściowej.