J'ai trouvé une approximation de racine carrée assez étrange mais qui fonctionne pour float
s; Je ne comprends vraiment pas. Quelqu'un peut-il m'expliquer pourquoi ce code fonctionne?
float sqrt(float f)
{
const int result = 0x1fbb4000 + (*(int*)&f >> 1);
return *(float*)&result;
}
Je l'ai testé un peu et il sort les valeurs de std::sqrt()
d'environ 1 à 3% . Je connais le Quake III racine carrée inverse rapide et je suppose que c'est quelque chose de similaire ici (sans l'itération newton) mais j'apprécierais vraiment une explication de comment il fonctionne .
(nota: je l'ai tagué à la fois c et c ++ car c'est à la fois valid-ish (voir commentaires) C et C++ code)
(*(int*)&f >> 1)
décale à droite la représentation au niveau du bit de f
. Ceci divise presque l'exposant par deux, ce qui équivaut approximativement à prendre la racine carrée.1
Pourquoi presque ? Dans IEEE-754, l'exposant réel est e - 127 .2 Pour diviser cela par deux, nous aurions besoin de e/2 - 64 , mais l'approximation ci-dessus nous donne seulement e/2 - 127 . Nous devons donc ajouter 63 à l'exposant résultant. Ceci est apporté par les bits 30-23 de cette constante magique (0x1fbb4000
).
J'imagine que les bits restants de la constante magique ont été choisis pour minimiser l'erreur maximale sur la plage de mantisse, ou quelque chose comme ça. Cependant, il n'est pas clair si cela a été déterminé analytiquement, itérativement ou heuristiquement.
Il convient de souligner que cette approche est quelque peu non portable. Il fait (au moins) les hypothèses suivantes:
float
.float
.Ainsi, il doit être évité à moins que vous ne soyez certain qu'il donne un comportement prévisible sur votre plate-forme (et en effet, qu'il fournit une accélération utile par rapport à sqrtf
!).
1. sqrt (a ^ b) = (a ^ b) ^ 0,5 = a ^ (b/2)
2. Voir par exemple https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Exponent_encoding
Voir l'explication d'Oliver Charlesworth sur les raisons pour lesquelles cela fonctionne presque . J'aborde une question soulevée dans les commentaires.
Étant donné que plusieurs personnes ont souligné la non-portabilité de cela, voici quelques façons de le rendre plus portable, ou au moins de faire en sorte que le compilateur vous dise si cela ne fonctionnera pas.
Tout d'abord, C++ vous permet de vérifier std::numeric_limits<float>::is_iec559
Au moment de la compilation, comme dans un static_assert
. Vous pouvez également vérifier que sizeof(int) == sizeof(float)
, ce qui ne sera pas vrai si int
est de 64 bits, mais ce que vous voulez vraiment faire est d'utiliser uint32_t
, Qui s'il existe sera toujours exactement 32 bits de large, aura un comportement bien défini avec des décalages et des débordements, et provoquera une erreur de compilation si votre architecture bizarre n'a pas un tel type intégral. Dans tous les cas, vous devez également static_assert()
que les types ont la même taille. Les assertions statiques n'ont aucun coût d'exécution et vous devriez toujours vérifier vos conditions préalables de cette façon si possible.
Malheureusement, le test pour savoir si la conversion des bits d'un float
en un uint32_t
Et le décalage est big-endian, little-endian ou aucun des deux ne peut pas être calculé comme une expression constante au moment de la compilation. Ici, je mets la vérification d'exécution dans la partie du code qui en dépend, mais vous voudrez peut-être la mettre dans l'initialisation et le faire une fois. En pratique, gcc et clang peuvent optimiser ce test lors de la compilation.
Vous ne voulez pas utiliser la distribution de pointeur non sécurisée, et il y a des systèmes sur lesquels j'ai travaillé dans le monde réel où cela pourrait planter le programme avec une erreur de bus. La façon la plus portable de convertir des représentations d'objets est d'utiliser memcpy()
. Dans mon exemple ci-dessous, je tape-pun avec un union
, qui fonctionne sur n'importe quelle implémentation réellement existante. (Les juristes de langue s'y opposent, mais aucun compilateur réussi ne cassera jamais autant de code hérité silencieusement .) Si vous devez faire une conversion de pointeur (voir ci-dessous) il y a alignas()
. Mais quelle que soit la façon dont vous le faites, le résultat sera défini par l'implémentation, c'est pourquoi nous vérifions le résultat de la conversion et du décalage d'une valeur de test.
Quoi qu'il en soit, pas que vous êtes susceptible de l'utiliser sur un processeur moderne, voici une version C++ 14 optimisée qui vérifie ces hypothèses non portables:
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <limits>
#include <vector>
using std::cout;
using std::endl;
using std::size_t;
using std::sqrt;
using std::uint32_t;
template <typename T, typename U>
inline T reinterpret(const U x)
/* Reinterprets the bits of x as a T. Cannot be constexpr
* in C++14 because it reads an inactive union member.
*/
{
static_assert( sizeof(T)==sizeof(U), "" );
union tu_pun {
U u = U();
T t;
};
const tu_pun pun{x};
return pun.t;
}
constexpr float source = -0.1F;
constexpr uint32_t target = 0x5ee66666UL;
const uint32_t after_rshift = reinterpret<uint32_t,float>(source) >> 1U;
const bool is_little_endian = after_rshift == target;
float est_sqrt(const float x)
/* A fast approximation of sqrt(x) that works less well for subnormal numbers.
*/
{
static_assert( std::numeric_limits<float>::is_iec559, "" );
assert(is_little_endian); // Could provide alternative big-endian code.
/* The algorithm relies on the bit representation of normal IEEE floats, so
* a subnormal number as input might be considered a domain error as well?
*/
if ( std::isless(x, 0.0F) || !std::isfinite(x) )
return std::numeric_limits<float>::signaling_NaN();
constexpr uint32_t magic_number = 0x1fbb4000UL;
const uint32_t raw_bits = reinterpret<uint32_t,float>(x);
const uint32_t rejiggered_bits = (raw_bits >> 1U) + magic_number;
return reinterpret<float,uint32_t>(rejiggered_bits);
}
int main(void)
{
static const std::vector<float> test_values{
4.0F, 0.01F, 0.0F, 5e20F, 5e-20F, 1.262738e-38F };
for ( const float& x : test_values ) {
const double gold_standard = sqrt((double)x);
const double estimate = est_sqrt(x);
const double error = estimate - gold_standard;
cout << "The error for (" << estimate << " - " << gold_standard << ") is "
<< error;
if ( gold_standard != 0.0 && std::isfinite(gold_standard) ) {
const double error_pct = error/gold_standard * 100.0;
cout << " (" << error_pct << "%).";
} else
cout << '.';
cout << endl;
}
return EXIT_SUCCESS;
}
Voici une autre définition de reinterpret<T,U>()
qui évite le type-punning. Vous pouvez également implémenter le jeu de mots en C moderne, où il est autorisé par la norme, et appeler la fonction en tant que extern "C"
. Je pense que le type-punning est plus élégant, sûr pour les caractères et compatible avec le style quasi fonctionnel de ce programme que memcpy()
. Je ne pense pas non plus que vous gagniez beaucoup, car vous pourriez toujours avoir un comportement indéfini à partir d'une représentation de piège hypothétique. De plus, clang ++ 3.9.1 -O -S est capable d'analyser statiquement la version de punition de type, d'optimiser la variable is_little_endian
En la constante 0x1
Et d'éliminer le test d'exécution, mais il ne peut optimiser cette version que sur un stub à instruction unique.
Mais plus important encore, ce code n'est pas garanti de fonctionner de manière portative sur tous les compilateurs. Par exemple, certains anciens ordinateurs ne peuvent même pas adresser exactement 32 bits de mémoire. Mais dans ces cas, il ne devrait pas être compilé et vous expliquer pourquoi. Aucun compilateur ne va soudainement casser une énorme quantité de code hérité sans raison. Bien que la norme donne techniquement l'autorisation de le faire et dise toujours qu'elle est conforme à C++ 14, cela ne se produira que sur une architecture très différente de ce que nous attendons. Et si nos hypothèses sont si invalides que certains compilateurs vont transformer un jeu de mots entre un float
et un entier non signé 32 bits en un bug dangereux, je doute vraiment que la logique derrière ce code se maintienne si nous utilisons simplement memcpy()
à la place. Nous voulons que ce code échoue au moment de la compilation et nous explique pourquoi.
#include <cassert>
#include <cstdint>
#include <cstring>
using std::memcpy;
using std::uint32_t;
template <typename T, typename U> inline T reinterpret(const U &x)
/* Reinterprets the bits of x as a T. Cannot be constexpr
* in C++14 because it modifies a variable.
*/
{
static_assert( sizeof(T)==sizeof(U), "" );
T temp;
memcpy( &temp, &x, sizeof(T) );
return temp;
}
constexpr float source = -0.1F;
constexpr uint32_t target = 0x5ee66666UL;
const uint32_t after_rshift = reinterpret<uint32_t,float>(source) >> 1U;
extern const bool is_little_endian = after_rshift == target;
Cependant, Stroustrup et al., Dans les Directives C++ Core , recommandent un reinterpret_cast
À la place:
#include <cassert>
template <typename T, typename U> inline T reinterpret(const U x)
/* Reinterprets the bits of x as a T. Cannot be constexpr
* in C++14 because it uses reinterpret_cast.
*/
{
static_assert( sizeof(T)==sizeof(U), "" );
const U temp alignas(T) alignas(U) = x;
return *reinterpret_cast<const T*>(&temp);
}
Les compilateurs que j'ai testés peuvent également optimiser cela à une constante pliée. Le raisonnement de Stroustrup est [sic]:
Accéder au résultat d'un
reinterpret_cast
À un type différent du type déclaré des objets est toujours un comportement indéfini, mais au moins nous pouvons voir que quelque chose de délicat se passe.
Soit y = sqrt (x),
il résulte des propriétés des logarithmes que log (y) = 0,5 * log (x) (1)
Interpréter un float
normal comme un entier donne INT (x) = Ix = L * (log (x) + B - σ) (2)
où L = 2 ^ N, N le nombre de bits de la signification, B est le biais de l'exposant et σ est un facteur libre pour régler l'approximation.
La combinaison de (1) et (2) donne: Iy = 0,5 * (Ix + (L * (B - σ)))
Qui est écrit dans le code comme (*(int*)&x >> 1) + 0x1fbb4000;
Trouvez le σ de sorte que la constante soit égale à 0x1fbb4000 et déterminez si elle est optimale.
Ajout d'un harnais de test wiki pour tester tous les float
.
L'approximation est à moins de 4% pour de nombreux float
, mais très médiocre pour les nombres inférieurs à la normale. YMMV
Worst:1.401298e-45 211749.20%
Average:0.63%
Worst:1.262738e-38 3.52%
Average:0.02%
Notez qu'avec un argument de +/- 0,0, le résultat n'est pas nul.
printf("% e % e\n", sqrtf(+0.0), sqrt_apx(0.0)); // 0.000000e+00 7.930346e-20
printf("% e % e\n", sqrtf(-0.0), sqrt_apx(-0.0)); // -0.000000e+00 -2.698557e+19
Code de test
#include <float.h>
#include <limits.h>
#include <math.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
float sqrt_apx(float f) {
const int result = 0x1fbb4000 + (*(int*) &f >> 1);
return *(float*) &result;
}
double error_value = 0.0;
double error_worst = 0.0;
double error_sum = 0.0;
unsigned long error_count = 0;
void sqrt_test(float f) {
if (f == 0) return;
volatile float y0 = sqrtf(f);
volatile float y1 = sqrt_apx(f);
double error = (1.0 * y1 - y0) / y0;
error = fabs(error);
if (error > error_worst) {
error_worst = error;
error_value = f;
}
error_sum += error;
error_count++;
}
void sqrt_tests(float f0, float f1) {
error_value = error_worst = error_sum = 0.0;
error_count = 0;
for (;;) {
sqrt_test(f0);
if (f0 == f1) break;
f0 = nextafterf(f0, f1);
}
printf("Worst:%e %.2f%%\n", error_value, error_worst*100.0);
printf("Average:%.2f%%\n", error_sum / error_count);
fflush(stdout);
}
int main() {
sqrt_tests(FLT_TRUE_MIN, FLT_MIN);
sqrt_tests(FLT_MIN, FLT_MAX);
return 0;
}