2013-04-17 4 views
4

J'essaie de calculer une approximation de la valeur epsilon pour le type float (et je sais que c'est déjà dans la bibliothèque standard).Arithmétique virgule flottante et machine epsilon

Les valeurs de epsilon sur cette machine sont (imprimées avec une certaine approximation):

FLT_EPSILON = 1.192093e-07 
DBL_EPSILON = 2.220446e-16 
LDBL_EPSILON = 1.084202e-19 

FLT_EVAL_METHOD est 2 si tout est fait en long double précision, et float, double et long double sont 32, 64 et 96 bits.

J'ai essayé d'obtenir une approximation de la valeur à partir de 1 et en le divisant par deux jusqu'à ce qu'il devienne trop petit, faire toutes les opérations avec le type float:

# include <stdio.h> 

int main(void) 
{ 
    float floatEps = 1; 

    while (1 + floatEps/2 != 1) 
     floatEps /= 2; 

    printf("float eps = %e\n", floatEps); 
} 

La sortie n'est pas ce que je recherchais pour:

float epsilon = 1.084202e-19 

opérations intermédiaires sont faites avec la plus grande précision (en raison de la valeur de FLT_EVAL_METHOD), de sorte que ce résultat semble légitime.

Cependant, ceci:

// 2.0 is a double literal 
while ((float) (1 + floatEps/2.0) != 1) 
    floatEps /= 2; 

donne cette sortie, ce qui est la bonne:

float epsilon = 1.192093e-07 

mais celui-ci:

// no double literals 
while ((float) (1 + floatEps/2) != 1) 
    floatEps /= 2; 

conduit à nouveau à un mauvais résultat, comme le premier:

float epsilon = 1.084202e-19 

Ces deux dernières versions devraient être équivalentes sur cette plateforme, est-ce un bug de compilateur? Si non, que se passe-t-il?

code

est compilé avec:

gcc -O0 -std=c99 -pedantic file.c 

La version gcc est assez vieux, mais je suis à l'université et je ne peux pas le mettre à jour:

$ gcc -v 
Using built-in specs. 
Target: i486-linux-gnu 
Configured with: ../src/configure -v --with-pkgversion='Debian 4.4.5-8' 
--with-bugurl=file:///usr/share/doc/gcc-4.4/README.Bugs 
--enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.4 
--enable-shared --enable-multiarch --enable-linker-build-id --with-system-zlib 
--libexecdir=/usr/lib --without-included-gettext --enable-threads=posix 
--with-gxx-include-dir=/usr/include/c++/4.4 --libdir=/usr/lib --enable-nls 
--enable-clocale=gnu --enable-libstdcxx-debug --enable-objc-gc 
--enable-targets=all --with-arch-32=i586 --with-tune=generic 
--enable-checking=release --build=i486-linux-gnu --host=i486-linux-gnu 
--target=i486-linux-gnu 
Thread model: posix 
gcc version 4.4.5 (Debian 4.4.5-8) 

La version actuelle de gcc, 4.7 , se comporte correctement sur mon ordinateur personnel. Il y a aussi des commentaires disant que différentes versions donnent des résultats différents. Après quelques réponses et commentaires, qui ont clarifié ce qui se passe comme prévu et ce qui ne l'est pas, j'ai légèrement changé la question pour la rendre plus claire.

+0

Quel compilateur utilisez-vous, et quels commutateurs utilisez-vous (cible, paramètres d'optimisation, et cetera)? Etes-vous positif que le code avec '(float) (1 + floatEps/2.)' et le code avec '(float) (1 + floatEps/2)' et aucune autre différence ne donne des résultats différents? Les deux fichiers source diffèrent uniquement dans ce seul caractère, et exactement les mêmes commutateurs de compilateur ont été utilisés? –

+0

@EricPostpischil bien, un ancien, j'ai ajouté des détails. Quoi qu'il en soit, j'ai testé le code à nouveau et je confirme les résultats. – effeffe

+0

Impossible de reproduire avec gcc 4.6.2. Le cast à 'float' produit la valeur correcte (' FLT_EVAL_METHOD' est 2, x86) pour les deux versions. –

Répondre

6

Le compilateur est autorisé à évaluer float expressions dans une plus grande précision qu'il aime, il semble donc que la première expression est évaluée en précision long double. Dans la deuxième expression, vous forcez à redimensionner le résultat à float.

En réponse à certaines de vos questions supplémentaires et à la discussion ci-dessous: vous recherchez essentiellement la plus petite différence non nulle avec 1 d'un certain type de virgule flottante.En fonction du paramètre FLT_EVAL_METHOD, un compilateur peut décider d'évaluer toutes les expressions à virgule flottante avec une précision supérieure aux types impliqués. Sur un Pentium traditionnellement, les registres internes de l'unité à virgule flottante sont à 80 bits et il est pratique d'utiliser cette précision pour tous les types à virgule flottante plus petits. Donc, à la fin de votre test dépend de la précision de votre comparaison !=. En l'absence d'une distribution explicite, la précision de cette comparaison est déterminée par votre compilateur et non par votre code. Avec la distribution explicite, vous faites évoluer la comparaison vers le type que vous désirez.

Comme vous avez confirmé que votre compilateur a défini FLT_EVAL_METHOD à 2, il utilise la plus grande précision pour tout calcul en virgule flottante. En guise de conclusion à la discussion ci-dessous, nous sommes confiants de dire qu'il y a un bug relatif à la mise en œuvre du cas FLT_EVAL_METHOD=2 dans gcc avant la version 4.5 et qui est corrigé à partir d'au moins la version 4.6. Si la constante entière 2 est utilisée dans l'expression au lieu de la constante à virgule flottante 2.0, la conversion en float est omise dans l'ensemble généré. Il convient également de noter qu'à partir du niveau d'optimisation -O1, les bons résultats sont produits sur ces anciens compilateurs, mais l'ensemble généré est assez différent et ne contient que quelques opérations en virgule flottante.

+0

Êtes-vous en train de dire que la distribution a effectivement réduit la précision de la valeur stockée? Si oui, pourquoi ai-je encore le mauvais résultat si je lance le résultat * sans * utiliser un double? (Je vais ajouter ceci à ma question) – effeffe

+0

"Le compilateur est autorisé à évaluer les expressions flottantes dans une plus grande précision qu'il aime" Cela dépend de la façon dont le compilateur a défini la macro 'FLT_EVAL_METHOD'. Le compilateur ne peut pas faire comme il aime, il doit faire exactement comme indiqué par comment il définit cette macro. –

+0

@PascalCuoq Vous avez raison, mais si la valeur de 'FLT_EVAL_METHOD' est' -1', elle peut à nouveau faire à peu près ce qu'elle veut. –

2

Un compilateur C99 C peut évaluer les expressions à virgule flottante comme si elles étaient d'un type à virgule flottante plus précis que leur type réel.

La FLT_EVAL_METHOD macro est défini par le compilateur pour indiquer la stratégie:

-1 indéterminable;

0 évaluer toutes les opérations et constantes juste à la gamme et la précision du type;

1 et évaluer les opérations constantes de type float et deux à la portée et la précision du type double , évaluer les opérations longues doubles et des constantes de la gamme et la précision de la longue type double;

2 évaluer toutes les opérations et les constantes à la gamme et la précision du long type double .

Pour des raisons historiques, deux choix communs en ciblant les processeurs x86 sont 0 et 2.

fichier m.c est votre premier programme. Si je compile, en utilisant mon compilateur, donc, j'obtiens:

$ gcc -std=c99 -mfpmath=387 m.c 
$ ./a.out 
float eps = 1.084202e-19 
$ gcc -std=c99 m.c 
$ ./a.out 
float eps = 1.192093e-07 

Si je compile cet autre programme ci-dessous, le compilateur définit la macro en fonction de ce qu'il fait:

#include <stdio.h> 
#include <float.h> 

int main(){ 
    printf("%d\n", FLT_EVAL_METHOD); 
} 

Résultats:

$ gcc -std=c99 -mfpmath=387 t.c 
$ ./a.out 
2 
$ gcc -std=c99 t.c 
$ ./a.out 
0 
+0

Donc, les deux derniers exemples ne devraient-ils pas donner le même résultat? Si toutes les constantes et opérations vivent dans 'long double' ... – effeffe

+0

@effeffe La distribution dans '(float) (1 + floatEps/2)' contraint le résultat de cette expression à être un 'float', même si la division et l'addition sont faites comme' long double'. Joseph S. Myers donne la meilleure interprétation de la façon dont les GCC modernes sont censés interpréter cette partie de la norme ici: http://gcc.gnu.org/ml/gcc-patches/2008-11/msg00105.html (et il cite C99: "Sauf pour l'assignation et la distribution (qui suppriment toute portée et précision supplémentaires) [...]") –

+0

Mais avec 'FLT_EVAL_METHOD == 2' ne devrait pas avoir' 2' et '2.' avoir le même effet, puisqu'il y a pas d'affectation ou de casting opérateur avant eux? (lien sympa) – effeffe