Rădăcină pătrată inversă rapidă (de asemenea rapid InvSqrt() sau 0x5F3759DF după constanta „magică” folosită , în zecimală 1 597 463 007) este un algoritm rapid aproximativ pentru calcularea rădăcinii pătrate inverse pentru numerele pozitive cu virgulă mobilă de 32 de biți . Algoritmul folosește operații cu numere întregi „scădere” și „ deplasare de biți ”, precum și „scădere” și „înmulțire” fracțională - fără operații lente „împărțire” și „rădăcină pătrată”. În ciuda faptului că este „hacky” la nivel de biți, aproximarea este monotonă și continuă : argumentele apropiate dau rezultate apropiate. Precizia (mai puțin de 0,2% în jos și niciodată în sus) [1] [2] nu este suficientă pentru calcule numerice reale, dar este destul de suficientă pentru grafica tridimensională .
Algoritmul ia ca intrare un număr în virgulă mobilă de 32 de biți ( precizie unică în format IEEE 754 ) și efectuează următoarele operații pe acesta:
Algoritmul original:
float Q_rsqrt ( număr float ) { i lung ; float x2 , y ; const float threehalfs = 1.5F ; x2 = număr * 0,5F ; y = număr ; i = * ( lung * ) & y ; // hacking evil flating point la nivel de biți i = 0x5f3759df - ( i >> 1 ); // ce dracu? y = * ( float * ) & i ; y = y * ( trei jumătăți - ( x2 * y * y ) ); // Prima iterație // y = y * ( trei jumătăți - ( x2 * y * y ) ); // A doua iterație, aceasta poate fi eliminată returnează y ; }Corect după standardele implementării moderne C, ținând cont de posibilele optimizări și cross-platform :
float Q_rsqrt ( număr float ) { const float x2 = număr * 0.5F ; const float threehalfs = 1.5F ; unire { plutire f ; uint32_t i ; } conv = { număr }; // membru „f” setat la valoarea „număr”. conv . i = 0x5f3759df - ( conv . i >> 1 ); conv . f *= trei jumătăți - x2 * conv . f * conv . f ; returnare conv . f ; }Implementarea de la Quake III: Arena [3] consideră că lungimea este egală cu , și folosește pointeri pentru conversie (optimizarea „dacă , nu s-a schimbat nimic” poate funcționa eronat; pe GCC, la compilare pentru „lansare”, este un avertisment declanșat). Și conține, de asemenea, un cuvânt obscen - John Carmack , așezând jocul în domeniul public, nu a înțeles ce se face acolo. floatlongfloatlong
În C++20 , puteți utiliza noua funcție . bit_cast
#include <bit> #include <limite> #include <cstdint> constexpr float Q_rsqrt ( număr float ) noexcept { static_assert ( std :: numeric_limits < float >:: is_iec559 ); // Verificați dacă mașina țintă este compatibilă float const y = std :: bit_cast < float > ( 0x5f3759df - ( std :: bit_cast < std :: uint32_t > ( număr ) >> 1 )); returnează y * ( 1,5f - ( număr * 0,5f * y * y )); }Algoritmul a fost probabil dezvoltat la Silicon Graphics în anii 1990, iar o implementare a apărut în 1999 în codul sursă pentru jocul de calculator Quake III Arena , dar metoda nu a apărut pe forumuri publice precum Usenet până în 2002-2003. Algoritmul generează rezultate rezonabil de precise folosind prima aproximare unică a metodei lui Newton . La acea vreme, principalul avantaj al algoritmului era respingerea calculelor costisitoare în virgulă mobilă în favoarea operațiilor cu numere întregi. Rădăcinile pătrate inverse sunt folosite pentru a calcula unghiurile de incidență și reflexie pentru iluminare și umbrire în grafica computerizată.
Algoritmul a fost atribuit inițial lui John Carmack , dar el a sugerat că Michael Abrash , specialist în grafică, sau Terje Mathisen, specialist în limbaj de asamblare, l- au adus la id Software . Studiul problemei a arătat că codul are rădăcini mai profunde atât în domeniul hardware, cât și în cel software al graficii computerizate. Corecțiile și modificările au fost făcute atât de Silicon Graphics, cât și de 3dfx Interactive , cea mai veche versiune cunoscută fiind scrisă de Gary Tarolli pentru SGI Indigo . Poate că algoritmul a fost inventat de Greg Walsh și Clive Moler , colegii lui Gary de la Ardent Computer [5] .
Jim Blinn, specialist în grafică 3D, a propus o metodă similară tabelară a rădăcinii pătrate inverse [6] care numără până la 4 cifre (0,01%) și a fost găsită la dezasamblarea jocului Interstate '76 (1997) [7] .
Odată cu lansarea 3DNow! Procesoarele AMD au introdus instrucțiunea de asamblare PFRSQRT [8] pentru calcularea rapidă aproximativă a rădăcinii pătrate inverse. Versiunea pentru dublu este lipsită de sens - acuratețea calculelor nu va crește [2] - prin urmare nu a fost adăugată. În 2000, funcția RSQRTSS [9] a fost adăugată la SSE2 , care este mai precisă decât acest algoritm (0,04% față de 0,2%).
Reprezentarea pe biți a unui număr fracționar de 4 octeți în format IEEE 754 arată astfel:
Semn | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Ordin | mantisa | |||||||||||||||||||||||||||||||
0 | 0 | unu | unu | unu | unu | unu | 0 | 0 | 0 | unu | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
31 | 24 | 23 | 16 | cincisprezece | opt | 7 | 0 |
Avem de-a face doar cu numere pozitive (bitul de semn este zero), nu denormalizate , nu ∞ și nu NaN . Astfel de numere sunt scrise în formă standard ca 1,mmmm 2 ·2 e . Partea 1,mmmm se numește mantisa , e este ordinul . Unitatea principală nu este stocată ( unitatea implicită ), așa că să numim valoarea 0,mmmm partea explicită a mantisei. În plus, numerele fracționale ale mașinii au o ordine deplasată : 2 0 se scrie ca 011.1111.1 2 .
Pe numerele pozitive , bijecția „fracțional ↔ întreg” (notat mai jos ca ) este continuă ca funcție liniară pe bucăți și monotonă . Din aceasta putem afirma imediat că rădăcina inversă rapidă, ca o combinație de funcții continue, este continuă. Și prima sa parte - deplasare-scădere - este, de asemenea, monotonă și liniară pe bucăți. Bijecția este complicată, dar aproape „gratuită”: în funcție de arhitectura procesorului și de convențiile de apelare , fie trebuie să nu faci nimic, fie să muți numărul din registrul fracționar în cel întreg.
De exemplu, reprezentarea binară a întregului hexazecimal 0x5F3759DF este 0|101.1111.0|011.0111.0101.1001.1101.1111 2 (punctele sunt granițe de ronțăit, liniile verticale sunt limite de câmp fracțional de computer). Ordinul lui 101 1111 0 2 este 190 10 , după scăderea decalajului 127 10 obținem exponentul 63 10 . Partea explicită a mantisei 01 101 110 101 100 111 011 111 2 după adăugarea unității conducătoare implicite devine 1.011 011 101 011 001 110 111 11 2 = 1.432 8… 4301 4 . Ținând cont de precizia reală a fracționalului computerizat 0x5F3759DF ↔ 1,4324301 10 2 63 .
Notăm partea explicită a mantisei a numărului , - ordinul neschimbat, - lungimea biților a mantisei, - offset al ordinului. Numărul , scris într-o rețea de biți liniar-logaritmică a fracțiilor computerizate, poate fi [10] [3] aproximat printr-o rețea logaritmică ca , unde este un parametru utilizat pentru a ajusta acuratețea aproximării. Acest parametru variază de la 0 (exact la și ) la 0,086 (exact la un punct, )
Folosind această aproximare, reprezentarea întregului număr poate fi aproximată ca
În consecință, .
Să facem același lucru [3] pentru (respectiv ) și să obținem
Constanta magică , ținând cont de granițe , în aritmetică fracțională va avea o ordine imparțială și mantisă ), iar în notație binară - 0|101.1111.0|01 1 ... (1 este o unitate implicită; 0,5 a venit din ordinea ; o unitate mică corespunde intervalului [1,375; 1,5) și, prin urmare, este foarte probabilă, dar nu este garantată de estimările noastre.)
Este posibil să se calculeze cu ce este egală prima aproximare liniară pe bucăți [11] (în sursă, nu mantisa în sine, dar este folosită partea sa explicită ):
La mai mare sau mai mică , rezultatul se modifică proporțional: atunci când este de patru ori , rezultatul este exact înjumătățit.
Metoda lui Newton dă [11] , , și . Funcția este descrescătoare și convexă în jos; pe astfel de funcții, metoda lui Newton se apropie de valoarea adevărată din stânga - prin urmare, algoritmul subestimează întotdeauna răspunsul.
Nu se știe de unde provine constanta 0x5F3759DF ↔ [a] 1,4324301 2 63 . Prin forța brută, Chris Lomont și Matthew Robertson au descoperit [1] [2] că cea mai bună constantă [b] în termeni de eroare relativă marginală pentru float este 0x5F375A86 ↔ 1.4324500 2 63 , pentru dublu este 0x5FE6EB50C7B537A9. Adevărat, pentru dublu algoritmul este lipsit de sens (nu oferă un câștig în precizie în comparație cu float) [2] . Constanta Lomont a fost obținută și analitic ( c = 1,432450084790142642179 ) [b] , dar calculele sunt destul de complicate [11] [2] .
După un pas al metodei lui Newton, rezultatul este destul de precis ( +0 % -0,18 % ) [1] [2] , ceea ce este mai mult decât potrivit pentru scopuri de grafică pe computer ( 1 ⁄ 256 ≈ 0,39 % ). O astfel de eroare este păstrată pe întregul interval de numere fracționale normalizate. Doi pași dau o precizie de 5 cifre [1] , după patru pași se ajunge la eroarea de dublu . Dacă doriți, puteți reechilibra eroarea înmulțind coeficienții 1,5 și 0,5 cu 1,0009, astfel încât metoda să dea simetric ±0,09% - asta au făcut în jocul Interstate '76 și metoda Blinn, care repetă și metoda lui Newton [7] ] .
Metoda lui Newton nu garantează monotonitatea, dar enumerarea computerizată arată că monotonitatea există.
Cod sursă (C++) #include <iostream> unire FloatInt { float asFloat ; int32_t asInt ; }; int floatToInt ( float x ) { FloatInt r ; r . asFloat = x ; întoarce r . asInt ; } float intToFloat ( int x ) { FloatInt r ; r . asInt = x ; întoarce r . asFloat ; } float Q_rsqrt ( număr float ) { i lung ; float x2 , y ; const float threehalfs = 1.5F ; x2 = număr * 0,5F ; y = număr ; i = * ( lung * ) & y ; // evil flating point bit level hacking i = 0x5f3759df - ( i >> 1 ); // ce dracu? y = * ( float * ) & i ; // nu stiu ce naiba! y = y * ( trei jumătăți - ( x2 * y * y ) ); // Prima iterație returnează y ; } int main () { int iStart = floatToInt ( 1.0 ); int iEnd = floatToInt ( 4.0 ); std :: cout << "Numerele de parcurs: " << iEnd - iStart << std :: endl ; int nProbleme = 0 ; float oldResult = std :: numeric_limits < float >:: infinity (); pentru ( int i = iStart ; i <= iEnd ; ++ i ) { float x = intToFloat ( i ); rezultat float = Q_rsqrt ( x ); if ( rezultat > oldResult ) { std :: cout << "Am găsit o problemă pe " << x << std :: endl ; ++ nProbleme ; } } std :: cout << "Total probleme: " << nProblems << std :: endl ; returnează 0 ; }Există algoritmi similari pentru alte puteri, cum ar fi rădăcina pătrată sau cubă [3] .
Suprapunerea „directă” a luminii pe un model tridimensional , chiar și unul high-poli, chiar și luând în considerare legea lui Lambert și alte formule de reflexie și împrăștiere, va da imediat un aspect poligonal - privitorul va vedea diferența de iluminare de-a lungul muchiile poliedrului [12] . Uneori, acest lucru este necesar - dacă obiectul este cu adevărat unghiular. Și pentru obiectele curbilinie, ele fac acest lucru: într-un program tridimensional, ele indică dacă o muchie ascuțită sau una netedă [12] . În funcție de aceasta, chiar și atunci când exportați modelul în joc, colțurile triunghiurilor calculează normala lungimii unității la suprafața curbată. În timpul animației și rotației, jocul transformă aceste valori normale împreună cu restul datelor 3D; la aplicarea luminii, se interpolează pe întregul triunghi și se normalizează (o aduce la o unitate de lungime).
Pentru a normaliza un vector, trebuie să împărțim toate cele trei componente ale acestuia la lungime. Sau, mai bine, înmulțiți-le cu reciproca lungimii: . Milioane din aceste rădăcini trebuie calculate pe secundă. Înainte ca hardware-ul dedicat transformării și procesării luminii să fie creat , software-ul de calcul ar putea fi lent. În special, la începutul anilor 1990, când a fost dezvoltat codul, majoritatea calculelor în virgulă mobilă au rămas în urma operațiilor cu numere întregi în performanță.
Quake III Arena folosește un algoritm rapid de rădăcină pătrată inversă pentru a accelera procesarea grafică de către CPU, dar algoritmul a fost implementat de atunci în unele vertex shadere hardware specializate folosind matrice programabile dedicate (FPGA).
Chiar și pe computerele anilor 2010, în funcție de sarcina coprocesorului fracționat , viteza poate fi de trei până la patru ori mai mare decât utilizarea funcțiilor standard [11] .