La racine carrée inverse rapide inhabituelle de John Carmack (Quake III)

112

John Carmack a une fonction spéciale dans le code source de Quake III qui calcule la racine carrée inverse d'un float, 4x plus rapide que normal (float)(1.0/sqrt(x)), y compris une 0x5f3759dfconstante étrange . Voir le code ci-dessous. Quelqu'un peut-il expliquer ligne par ligne ce qui se passe exactement ici et pourquoi cela fonctionne beaucoup plus rapidement que la mise en œuvre régulière?

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;
}
Alex
la source
10
Cela a été écrit des millions de fois. Voir: google.com/search?q=0x5f3759df
Greg Hewgill
15
Merci quand même. C'était une question beaucoup plus intéressante que "comment rendre un nombre positif négatif en C #?"
MusiGenesis
9
Ce n'était pas Carmack. en.wikipedia.org/wiki/Fast_inverse_square_root
h4xxr
7
Putain de merde, ceci est juste un hack basé sur la méthode de Newton, ce n'est pas un Saint Graal d'algorithmes, arrêtez d'en parler pleas: P
ldog

Réponses:

75

FYI. Carmack ne l'a pas écrit. Terje Mathisen et Gary Tarolli en ont tous les deux un crédit partiel (et très modeste), ainsi que d'autres sources.

Comment la constante mythique a été dérivée est quelque chose d'un mystère.

Pour citer Gary Tarolli:

Ce qui fait en fait un calcul en virgule flottante en nombre entier - il a fallu beaucoup de temps pour comprendre comment et pourquoi cela fonctionne, et je ne me souviens plus des détails.

Une constante légèrement meilleure, développée par un mathématicien expert (Chris Lomont) essayant de comprendre le fonctionnement de l'algorithme d'origine est:

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;
}

Malgré cela, sa tentative initiale d'une version mathématiquement «supérieure» du sqrt d'id (qui atteignait presque la même constante) s'est avérée inférieure à celle initialement développée par Gary bien qu'elle soit mathématiquement beaucoup plus «pure». Il ne pouvait pas expliquer pourquoi les id étaient si excellents.

Rushyo
la source
4
Que signifie «mathématiquement plus pur»?
Tara
1
J'imagine où la première estimation peut être dérivée de constantes justifiables, plutôt que d'être en apparence arbitraire. Bien que si vous voulez une description technique, vous pouvez la rechercher. Je ne suis pas mathématicien et une discussion sémantique sur la terminologie mathématique n'appartient pas à SO.
Rushyo
7
C'est exactement la raison pour laquelle j'ai encapsulé ce mot entre guillemets effrayants, pour éviter ce genre d'absurdités. Cela suppose que le lecteur est familier avec l'écriture anglaise familière, je suppose. Vous penseriez que le bon sens serait suffisant. Je n'ai pas utilisé un terme vague parce que je pensais "vous savez quoi, je veux vraiment être interrogé à ce sujet par quelqu'un qui ne peut pas être dérangé pour rechercher la source originale qui prendrait deux secondes sur Google".
Rushyo
2
En fait, vous n'avez pas répondu à la question.
BJovke
1
Pour ceux qui voulaient savoir où il le trouve: Beyond3d.com/content/articles/8
mr5
52

Bien sûr, ces jours-ci, cela s'avère beaucoup plus lent que d'utiliser simplement le sqrt d'un FPU (en particulier sur 360 / PS3), car le basculement entre les registres float et int induit un load-hit-store, tandis que l'unité à virgule flottante peut faire un carré réciproque root dans le matériel.

Il montre simplement comment les optimisations doivent évoluer en fonction de la nature des changements matériels sous-jacents.

Crashworks
la source
4
C'est quand même beaucoup plus rapide que std :: sqrt ().
Tara
2
Avez-vous une source? Je souhaite tester les environnements d'exécution mais je n'ai pas de kit de développement Xbox 360.
DucRP
31

Greg Hewgill et IllidanS4 ont donné un lien avec une excellente explication mathématique. Je vais essayer de résumer ici pour ceux qui ne veulent pas trop entrer dans les détails.

Toute fonction mathématique, à quelques exceptions près, peut être représentée par une somme polynomiale:

y = f(x)

peut être exactement transformé en:

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

Où a0, a1, a2, ... sont des constantes . Le problème est que pour de nombreuses fonctions, comme la racine carrée, pour une valeur exacte cette somme a un nombre infini de membres, elle ne se termine pas à un certain x ^ n . Mais, si nous nous arrêtons à un x ^ n, nous aurions toujours un résultat avec une certaine précision.

Donc, si nous avons:

y = 1/sqrt(x)

Dans ce cas particulier, ils ont décidé de rejeter tous les membres polynomiaux au-dessus de la seconde, probablement à cause de la vitesse de calcul:

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

Et la tâche consiste maintenant à calculer a0 et a1 afin que y ait le moins de différence avec la valeur exacte. Ils ont calculé que les valeurs les plus appropriées sont:

a0 = 0x5f375a86
a1 = -0.5

Donc, lorsque vous mettez cela dans l'équation, vous obtenez:

y = 0x5f375a86 - 0.5*x

Qui est la même que la ligne que vous voyez dans le code:

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

Edit: en fait, ce y = 0x5f375a86 - 0.5*xn'est pas la même chose que i = 0x5f375a86 - (i >> 1);puisque le décalage de float comme entier non seulement divise par deux, mais divise également l'exposant par deux et provoque d'autres artefacts, mais cela revient toujours à calculer certains coefficients a0, a1, a2 ....

À ce stade, ils ont découvert que la précision de ce résultat n'est pas suffisante à cet effet. Ainsi, ils n'ont en outre fait qu'une seule étape de l'itération de Newton pour améliorer la précision du résultat:

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

Ils auraient pu faire d'autres itérations en boucle, chacune améliorant le résultat, jusqu'à ce que la précision requise soit atteinte. C'est exactement comme cela que cela fonctionne dans CPU / FPU! Mais il semble qu'une seule itération ait suffi, ce qui était également une bénédiction pour la vitesse. CPU / FPU fait autant d'itérations que nécessaire pour atteindre la précision du nombre à virgule flottante dans lequel le résultat est stocké et il a un algorithme plus général qui fonctionne dans tous les cas.


Donc, en bref, ce qu'ils ont fait est:

Utilisez (presque) le même algorithme que CPU / FPU, exploitez l'amélioration des conditions initiales pour le cas particulier de 1 / sqrt (x) et ne calculez pas jusqu'à la précision CPU / FPU ira mais arrêtez plus tôt, donc gagner en vitesse de calcul.

BJovke
la source
2
Le cast du pointeur vers long est une approximation de log_2 (float). Le renvoyer est une longueur approximative de 2 ^. Cela signifie que vous pouvez rendre le rapport approximativement linéaire.
wizzwizz4
22

D' après ce bel article écrit il y a quelque temps ...

La magie du code, même si vous ne pouvez pas le suivre, se distingue par le i = 0x5f3759df - (i >> 1); ligne. Simplifié, Newton-Raphson est une approximation qui commence par une estimation et l'affine avec l'itération. Tirant parti de la nature des processeurs x86 32 bits, i, un entier, est initialement défini sur la valeur du nombre à virgule flottante dont vous voulez prendre le carré inverse, en utilisant un cast entier. i est alors mis à 0x5f3759df, moins lui-même décalé d'un bit vers la droite. Le décalage à droite supprime le bit le moins significatif de i, le divisant essentiellement par deux.

C'est une très bonne lecture. Ce n'est qu'un tout petit morceau de celui-ci.

Dillie-O
la source
19

J'étais curieux de voir ce qu'était la constante en tant que flottant, alors j'ai simplement écrit ce morceau de code et googlé l'entier qui apparaissait.

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

Il semble que la constante soit "Une approximation entière de la racine carrée de 2 ^ 127 mieux connue sous la forme hexadécimale de sa représentation en virgule flottante, 0x5f3759df" https://mrob.com/pub/math/numbers-18.html

Sur le même site, il explique tout. https://mrob.com/pub/math/numbers-16.html#le009_16

Cette question est vraiment ancienne
la source
6
Cela mérite plus d'attention. Tout cela a du sens après avoir réalisé que ce n'est que la racine carrée de 2 ^ 127 ...
u8y7541