John Carmack a une fonction spéciale dans le code source de Quake III qui calcule la racine carrée inverse d'un flotteur, 4x plus rapide que le (float)(1.0/sqrt(x))
, y compris un étrange 0x5f3759df
constant. Voir le code ci-dessous. Quelqu'un peut-il expliquer ligne par ligne ce qui se passe exactement ici et pourquoi cela fonctionne tellement plus rapidement que l'implémentation 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;
}
Pour info. Carmack ne l'a pas écrit. Terje Mathisen et Gary Tarolli en attribuent tous 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 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 comment l'algorithme original fonctionnait 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 est presque devenue 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 id était si excellent iirc.
Bien sûr, de nos jours, il s'avère être beaucoup plus lent que de simplement utiliser le sqrt d'un FPU (en particulier sur 360/PS3), car le basculement entre les registres float et int induit une charge-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 à mesure que la nature du matériel sous-jacent change.
Greg Hewgill et IllidanS4 ont donné un lien avec une excellente explication mathématique. Je vais essayer de le 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 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 par quelque x ^ n. Mais, si nous nous arrêtons à quelque x ^ n nous aurions toujours un résultat jusqu'à une certaine précision.
Donc, si nous avons:
y = 1/sqrt(x)
Dans ce cas particulier, ils ont décidé de supprimer tous les membres polynomiaux au-dessus de la seconde, probablement en raison de la vitesse de calcul:
y = a0 + a1*x + [...discarded...]
Et la tâche est maintenant venue de calculer a0 et a1 afin que y ait le moins de différence par rapport à la valeur exacte. Ils ont calculé que les valeurs les plus appropriées sont:
a0 = 0x5f375a86
a1 = -0.5
Ainsi, lorsque vous mettez cela en é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 ici y = 0x5f375a86 - 0.5*x
N'est pas la même chose que i = 0x5f375a86 - (i >> 1);
puisque le décalage de float en entier non seulement divise par deux mais divise également l'exposant par deux et provoque certains 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 pour le but. Ils n'ont donc fait qu'une seule étape de l'itération de Newton pour améliorer la précision des résultats:
x = x * (1.5f - xhalf * x * x)
Ils auraient pu faire plus d'itérations dans une boucle, chacune améliorant le résultat, jusqu'à ce que la précision requise soit atteinte. C'est exactement comme ça que ça marche dans le CPU/FPU! Mais il semble qu'une seule itération était suffisante, ce qui était également une bénédiction pour la vitesse. Le CPU/FPU effectue 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 dispose d'un algorithme plus général qui fonctionne dans tous les cas.
Bref, ce qu'ils ont fait, c'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 tout le chemin vers la précision CPU/FPU ira mais s'arrêtera plus tôt, gagnant ainsi en vitesse de calcul.
Selon à cet article de Nice é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 supposition 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 une conversion d'entiers. i est alors réglé sur 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 par deux.
C'est une très bonne lecture. Ce n'est qu'un tout petit morceau.
J'étais curieux de voir quelle était la constante sous forme de flottant, j'ai donc simplement écrit ce morceau de code et googlé l'entier qui est sorti.
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 le tout. https://mrob.com/pub/math/numbers-16.html#le009_16