Radice quadrata inversa veloce insolita di John Carmack (Quake III)


112

John Carmack ha una funzione speciale nel codice sorgente di Quake III che calcola la radice quadrata inversa di un float, 4 volte più veloce del normale (float)(1.0/sqrt(x)), inclusa una strana 0x5f3759dfcostante. Vedi il codice qui sotto. Qualcuno può spiegare riga per riga cosa sta succedendo esattamente qui e perché funziona così molto più velocemente della normale implementazione?

float Q_rsqrt( float number )
{
  long i;
  float x2, y;
  const float threehalfs = 1.5F;

  x2 = number * 0.5F;
  y  = number;
  i  = * ( long * ) &y;
  i  = 0x5f3759df - ( i >> 1 );
  y  = * ( float * ) &i;
  y  = y * ( threehalfs - ( x2 * y * y ) );

  #ifndef Q3_VM
  #ifdef __linux__
    assert( !isnan(y) );
  #endif
  #endif
  return y;
}


10
Questo è stato scritto milioni di volte. Vedi: google.com/search?q=0x5f3759df
Greg Hewgill

15
Grazie comunque. Questa era una domanda molto più interessante di "come si fa a rendere negativo un numero positivo in C #?"
MusiGenesis


7
Santo cielo, questo è solo un trucco basato sul metodo di Newton, non è un santo graal di algoritmi, smettila di parlarne suppliche: P
ldog

Risposte:


75

FYI. Carmack non l'ha scritto. Terje Mathisen e Gary Tarolli ne prendono entrambi un merito parziale (e molto modesto), oltre ad accreditare altre fonti.

Il modo in cui è stata derivata la costante mitica è qualcosa di misterioso.

Per citare Gary Tarolli:

Che in realtà sta eseguendo un calcolo in virgola mobile in numero intero - ci è voluto molto tempo per capire come e perché funziona, e non riesco più a ricordare i dettagli.

Una costante leggermente migliore, sviluppata da un matematico esperto (Chris Lomont) che cerca di capire come funzionava l'algoritmo originale, è:

float InvSqrt(float x)
{
    float xhalf = 0.5f * x;
    int i = *(int*)&x;              // get bits for floating value
    i = 0x5f375a86 - (i >> 1);      // gives initial guess y0
    x = *(float*)&i;                // convert bits back to float
    x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
    return x;
}

Nonostante ciò, il suo tentativo iniziale di una versione matematicamente "superiore" di sqrt di id (che arrivò quasi alla stessa costante) si dimostrò inferiore a quella inizialmente sviluppata da Gary, nonostante fosse matematicamente molto "più puro". Non riusciva a spiegare perché id's fosse così eccellente iirc.


4
Cosa dovrebbe significare "matematicamente più puro"?
Tara

1
Immagino dove la prima ipotesi possa essere derivata da costanti giustificabili, piuttosto che essere apparentemente arbitraria. Anche se vuoi una descrizione tecnica, puoi cercarla. Non sono un matematico e una discussione semantica sulla terminologia matematica non appartiene a SO.
Rushyo

7
Questo è esattamente il motivo per cui ho racchiuso quella parola tra virgolette spaventose, per evitare questo genere di sciocchezze. Ciò presuppone che il lettore abbia familiarità con la scrittura inglese colloquiale, immagino. Penseresti che il buon senso sarebbe sufficiente. Non ho usato un termine vago perché ho pensato "sai cosa, voglio davvero essere interrogato su questo da qualcuno che non può essere disturbato a cercare la fonte originale che richiederebbe due secondi su Google".
Rushyo

2
Beh, in realtà non hai risposto alla domanda.
BJovke

1
Per chi volesse sapere dove lo trova: beyond3d.com/content/articles/8
mr5

52

Ovviamente in questi giorni, risulta essere molto più lento del semplice utilizzo di sqrt di una FPU (specialmente su 360 / PS3), perché lo scambio tra registri float e int induce un load-hit-store, mentre l'unità in virgola mobile può fare quadrato reciproco root nell'hardware.

Mostra solo come devono evolversi le ottimizzazioni in base alla natura dei cambiamenti hardware sottostanti.


4
Tuttavia, è ancora molto più veloce di std :: sqrt ().
Tara

2
Hai una fonte? Voglio testare i tempi di esecuzione ma non ho un kit di sviluppo per Xbox 360.
DucRP

31

Greg Hewgill e IllidanS4 hanno fornito un collegamento con un'eccellente spiegazione matematica. Cercherò di riassumere qui per quelli che non vogliono entrare troppo nei dettagli.

Qualsiasi funzione matematica, con alcune eccezioni, può essere rappresentata da una somma polinomiale:

y = f(x)

può essere esattamente trasformato in:

y = a0 + a1*x + a2*(x^2) + a3*(x^3) + a4*(x^4) + ...

Dove a0, a1, a2, ... sono costanti . Il problema è che per molte funzioni, come la radice quadrata, per un valore esatto questa somma ha un numero infinito di membri, non termina con x ^ n . Ma se ci fermassimo a qualche x ^ n avremmo comunque un risultato con una certa precisione.

Quindi, se abbiamo:

y = 1/sqrt(x)

In questo caso particolare hanno deciso di scartare tutti i membri del polinomio sopra il secondo, probabilmente a causa della velocità di calcolo:

y = a0 + a1*x + [...discarded...]

E il compito è ora di calcolare a0 e a1 in modo che y abbia la minima differenza dal valore esatto. Hanno calcolato che i valori più appropriati sono:

a0 = 0x5f375a86
a1 = -0.5

Quindi, quando lo metti in equazione, ottieni:

y = 0x5f375a86 - 0.5*x

Che è la stessa della riga che vedi nel codice:

i = 0x5f375a86 - (i >> 1);

Modifica: in realtà qui y = 0x5f375a86 - 0.5*xnon è la stessa cosa i = 0x5f375a86 - (i >> 1);poiché lo spostamento di float come numero intero non solo divide per due ma divide anche l'esponente per due e causa altri artefatti, ma si tratta comunque di calcolare alcuni coefficienti a0, a1, a2 ....

A questo punto hanno scoperto che la precisione di questo risultato non è sufficiente allo scopo. Quindi hanno anche fatto solo un passaggio dell'iterazione di Newton per migliorare l'accuratezza del risultato:

x = x * (1.5f - xhalf * x * x)

Avrebbero potuto eseguire più iterazioni in un ciclo, ciascuna migliorando il risultato, fino a quando non è stata raggiunta la precisione richiesta. Questo è esattamente come funziona in CPU / FPU! Ma sembra che sia bastata una sola iterazione, che è stata anche una benedizione per la velocità. CPU / FPU esegue tutte le iterazioni necessarie per raggiungere la precisione per il numero in virgola mobile in cui è memorizzato il risultato e ha un algoritmo più generale che funziona per tutti i casi.


Quindi, in breve, quello che hanno fatto è:

Usa (quasi) lo stesso algoritmo di CPU / FPU, sfrutta il miglioramento delle condizioni iniziali per il caso speciale di 1 / sqrt (x) e non calcolare fino in fondo alla precisione che CPU / FPU andrà a ma fermarsi prima, quindi guadagnando velocità di calcolo.


2
Lanciare il puntatore su long è un'approssimazione di log_2 (float). Il rigetto è un'approssimazione di 2 ^ lunghezza. Ciò significa che puoi rendere il rapporto approssimativamente lineare.
wizzwizz4

22

Secondo questo bell'articolo scritto tempo fa ...

La magia del codice, anche se non puoi seguirlo, risalta come i = 0x5f3759df - (i >> 1); linea. Semplificato, Newton-Raphson è un'approssimazione che inizia con un'ipotesi e la perfeziona con l'iterazione. Sfruttando la natura dei processori x86 a 32 bit, i, un numero intero, viene inizialmente impostato sul valore del numero in virgola mobile di cui si desidera prendere il quadrato inverso, utilizzando un cast intero. i è quindi impostato su 0x5f3759df, meno se stesso spostato di un bit a destra. Lo spostamento a destra elimina il bit meno significativo di i, essenzialmente dimezzandolo.

È davvero una buona lettura. Questo è solo un piccolo pezzo.


19

Ero curioso di vedere quale fosse la costante come float, quindi ho semplicemente scritto questo bit di codice e ho cercato su Google l'intero che è saltato fuori.

    long i = 0x5F3759DF;
    float* fp = (float*)&i;
    printf("(2^127)^(1/2) = %f\n", *fp);
    //Output
    //(2^127)^(1/2) = 13211836172961054720.000000

Sembra che la costante sia "Un'approssimazione intera alla radice quadrata di 2 ^ 127 meglio conosciuta dalla forma esadecimale della sua rappresentazione in virgola mobile, 0x5f3759df" https://mrob.com/pub/math/numbers-18.html

Sullo stesso sito spiega il tutto. https://mrob.com/pub/math/numbers-16.html#le009_16


6
Questo merita più attenzione. Tutto ha senso dopo aver realizzato che è solo la radice quadrata di 2 ^ 127 ...
u8y7541
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.