Voir l'explication d'Oliver Charlesworth sur la raison pour laquelle ceci presque fonctionne. Je réponds à une question soulevée dans les commentaires.
Comme plusieurs personnes ont souligné la non-portabilité de ce système, voici quelques moyens de le rendre plus portable, ou au moins de faire en sorte que le compilateur vous indique si cela ne fonctionne pas.
Tout d'abord, le 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 c'est utiliser uint32_t
qui, s'il existe, sera toujours d'une largeur de 32 bits exactement, aura un comportement bien défini avec les décalages et les débordements, et provoquera une erreur de compilation si votre architecture bizarre ne possède pas un tel type d'intégrale. Dans tous les cas, vous devriez é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 préconditions de cette façon si possible.
Malheureusement, le test pour savoir si la conversion des bits d'une float
à un uint32_t
et le décalage est big-endian, little-endian ou ni l'un ni l'autre ne peut pas être calculé comme une expression constante du temps de compilation. Ici, j'ai mis la vérification au moment de l'exécution dans la partie du code qui en dépend, mais vous pourriez vouloir la mettre dans l'initialisation et la faire une fois. En pratique, gcc et clang peuvent tous deux optimiser ce test au moment de la compilation.
Vous ne voulez pas utiliser le "unsafe pointer cast", et il y a certains systèmes sur lesquels j'ai travaillé dans le monde réel où cela pourrait faire planter le programme avec une erreur de bus. La façon la plus portable de convertir les représentations d'objets est avec memcpy()
. Dans mon exemple ci-dessous, je tape-pun avec un union
qui fonctionne sur toute implémentation existante. (Les avocats spécialisés dans les langues s'y opposent, mais aucun compilateur performant ne cassera jamais autant de code existant. en silence .) 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, bien que vous ne soyez pas susceptible de l'utiliser sur un processeur moderne, voici une version C++14 amélioré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;
}
Mise à jour
Voici une autre définition de reinterpret<T,U>()
qui évite le détournement de type. Vous pourriez également implémenter le détournement de type en C moderne, où il est autorisé par la norme, et appeler la fonction comme suit extern "C"
. Je pense que la ruse de type est plus élégante, plus sûre et plus cohérente avec le style quasi-fonctionnel de ce programme que la ruse de type. memcpy()
. Je ne pense pas non plus que vous gagniez beaucoup, car vous pourriez toujours avoir un comportement indéfini à partir d'une hypothétique représentation piège. Par ailleurs, clang++ 3.9.1 -O -S est capable d'analyser statiquement la version de type-punning, d'optimiser la variable is_little_endian
à la constante 0x1
et éliminer le test d'exécution, mais il ne peut optimiser cette version que jusqu'à un stub à instruction unique.
Mais plus important encore, ce code n'est pas garanti pour fonctionner de manière portable sur tous les compilateurs. Par exemple, certains vieux ordinateurs ne peuvent même pas adresser exactement 32 bits de mémoire. Mais dans ces cas-là, il devrait échouer à la compilation et vous dire pourquoi. Aucun compilateur ne va soudainement casser une énorme quantité de code hérité sans raison. Bien que la norme donne techniquement la permission de le faire et de continuer à dire qu'il est conforme à C++14, cela ne se produira que sur une architecture très différente de celle à laquelle nous nous attendons. Et si nos hypothèses sont si invalides qu'un compilateur va transformer un type-pun entre un float
et un entier non signé de 32 bits en un bug dangereux, je doute vraiment que la logique derrière ce code tienne si nous utilisons simplement memcpy()
à la place. Nous voulons que ce code échoue à la compilation et qu'il nous dise 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;
Toutefois, Stroustrup et al. Directives de base du C++ recommande 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 cette fonction en la réduisant à une constante repliée. Le raisonnement de Stroustrup est [sic] :
Accéder au résultat d'un reinterpret_cast
à un type différent du type déclaré de l'objet est toujours un comportement non défini, mais au moins nous pouvons voir que quelque chose de délicat se passe.
25 votes
Ce n'est ni du C valide ni du C++ valide. Il enfreint les règles d'aliasing et suppose une représentation particulière pour les valeurs à virgule flottante et pour le
int
valeurs. Cela en fait du code de hackerhead, qui est parfois intriguant mais qui ne doit généralement pas être émulé.7 votes
C'est une sorte d'ami de la autre numéro magique
0x5f3759df
2 votes
Ressemble à "Approximations qui dépendent de la représentation en virgule flottante" comme décrit dans fr.wikipedia.org/wiki/Méthodes_de_calcul_des_racines_carrées
11 votes
De manière approximative, le décalage vers la droite de la représentation binaire de
f
divise l'exposant par deux, ce qui équivaut à prendre la racine carrée. Tout le reste est vraisemblablement de la magie pour améliorer la précision sur toute la gamme des mantisses.5 votes
divise l'exposant par deux, ce qui équivaut à prendre la racine carrée. ce que
2 votes
F = m * e^x, sqrt(f) = sqrt(m) * e^(x / 2)
12 votes
@Fureeish - sqrt(a^b) = (a^b)^0.5 = a^(b/2).
2 votes
@Fureeish : C'est ce que le carré Root signifie !
2 votes
@BoundaryImposition il peut s'agir d'une approximation de la racine carrée mais il est évident que la simple division de l'exposant n'est pas ce que signifie la racine carrée. Le commentaire de Goswin von Brederlow est plus correct.
3 votes
@LuuVinhPhúc : Pour les nombres positifs, c'est littéralement le cas. sqrt(x) est x^0.5 si x est positif. C'est une équivalence stricte et insécable, pas une "certaine approximation". Il est vrai que les choses se compliquent pour les autres x.
3 votes
@BoundaryImposition mais il s'agit de la valeur IEEE-754 en
m * e^x
et non un nombre arbitraire de x0 votes
L'article mentionne que le véritable chiffre magique de cette méthode est le suivant
0x5f375a86
.5 votes
@PeteBecker : C'est un code C et C++ absolument valide. Cependant, son comportement est défini par l'implémentation. Ne confondez pas invalide et non-portable ; ce n'est pas la même chose.
4 votes
@JackAidley -- son comportement est indéfini car il viole les règles strictes d'aliasing. C'est le genre de code que les gens écrivaient il y a 30 ans, à l'époque où le codage en C était sauvage et nébuleux, mais la définition du langage a changé depuis. Et notez que "implementation defined" signifie que l'implémentation doit documenter ce qu'elle fait. Il n'y a pas de telle exigence pour ce type de distribution.
0 votes
@YSC Ceci question montre le principe général de construction de ce type d'approximation.