OVH Cloud OVH Cloud

comparaison de flottants

209 réponses
Avatar
Emmanuel
bonjour tout le monde,

une question de vrai débutant :

Avec le programme essai.c suivant

int main(void) {
return ((1.7 + 0.1) == 1.8) ? 1 : 0;
}

j'obtiens :

$ gcc -Wall -o essai essai.c; ./essai; echo $?
1

Avec le programme essai.c suivant

int main(void) {
float x;
x = 1.7;
return ((x + 0.1) == 1.8) ? 1 : 0;
}

j'obtiens :

$ gcc -Wall -o essai essai.c; ./essai; echo $?
0

Pourtant, le programme

#include <stdio.h>
int main(void) {
float x;
x = 1.7;
printf("%f et %f\n", 1.7 + 0.1, x + 0.1);
}

affiche :
1.800000 et 1.800000

Apparremment, 1.7+0.1 et x+0.1 sont égaux ; j'imagine qu'il s'agit d'un
problème de représentation des flottants en machine mais j'aimerais bien
avoir une confirmation ou une infirmation (et une explication, si possible).

Merci par avance.

Emmanuel

10 réponses

1 2 3 4 5
Avatar
Vincent Lefevre
Dans l'article <4bb28bba$0$29967$,
Samuel DEVULDER écrit:

Vincent Lefevre a écrit :

>> http://www.abxsoft.com/misra/misra-test.html
>> /* Rule 50: Required */
>> /* Floating point variables shall not be tested for exact equality or */
>> /* inequality. */
>> MISRA-C [1998:2004] Function Definition: rule50
>
> Il y a des cas où le test d'égalité a un sens. Mais bon...

Sans doute. Mais si tu as des exemples concrets ca m'interresse



Le cas classique:

if (x != y)
a = b / (x - y);
else
...

On a aussi x != x pour tester si x est NaN.

Aussi après un appel de rint...

Après, on peut passer dans des algo avancés, où on contrôle l'erreur.
Un exemple dans le code de CRlibm:

if(logh == (logh + (logm * RNROUNDCST)))

car en fait je me dit que si on veut une équalité stricte, alors il
est plus simple de passer par un union et comparer (bit à bits) les
entiers correspondants.



Non! D'une part c'est incorrect, car les représentations ne sont pas
forcément identiques (encore plus s'il y a des bytes de padding, comme
avec un long double en 80 bits dans une union). D'autre part, ce serait
plus lent.

Car en fait quand on utilise les floats/doubles, il y a parfois des
surprises entre ce qu'on croit que le compilo fait, et ce qu'il fait
vraiment.



Oui, il faut faire attention.

> Faut bien se dire que de tels "standards de codages industriels"
> ont leur spécificité. À la même URL:
>
> /* Rule 13: Advisory */
> /* The basic types char, short, int, long, float and double should not */
> /* be used. */

Oui le standard est utilisé dans les calculateurs automobiles. Là dedans
tu as des ECU 8bits, 16bits, 32bits et voir plus. Le problème c'est les
types C ne sont pas assez précis concernant le nombre de bits
indépendamment de l'architecture. Le standards recommande donc (cf le
Advisory) d'utiliser les versions indiquant le nombre de bits: uint32,
int8, etc.



Des types de <stdint.h> pourraient être utilisés. Mais c'est peut-être
trop nouveau...

--
Vincent Lefèvre - Web: <http://www.vinc17.net/>
100% accessible validated (X)HTML - Blog: <http://www.vinc17.net/blog/>
Work: CR INRIA - computer arithmetic / Arénaire project (LIP, ENS-Lyon)
Avatar
Samuel DEVULDER
Vincent Lefevre a écrit :
Dans l'article <4bb29128$0$14657$,
Samuel DEVULDER écrit:

Cela dit suivant le parseur utilisé on peut avoir des soucis car la
partie "0.5" peut être traduite par un 5*0.1, avec le 0.1 qui n'est pas
exact.



Ce serait un bug.



Peut-etre.. qu'est il préconisé à la place? Parce que les codes pour
parser la partie fractionnaire où l'on trouve:
double b = 0.1;
while(..) {
x += ((*s++) - '0') * b;
b /= 10;
}
sont légions et contiennent tous le même bug.


Beurk. Utiliser %a (ISO C99).





Ah? oui peut-être.. mais c'est un détail superflu pour le sujet à mon
avis. D'ailleurs moi je voulais imprimer des *entiers* et pas des
flottants en hexa, donc %a est hors sujet.



Le code est tout de même faux sur la plupart des machines actuelles
(avec des long sur 64 bits)!



Hum? Je ne vois pas. D'un coté tu as 2 long int à 32 bits chacun, et de
l'autre 64bits. Ca doit rentrer.

Et puis je trouve la notation flottant
hexa plus parlante que l'encodage brut: s'il y a une différence entre
deux valeurs, on voit mieux à quoi elle correspond.




Oui, c'est pas faux. En tout cas on se fait moins suer avec le
byte-order (mais j'ai écrit que mon propos n'était pas de retrouver
l'exposant là dedans, mais uniquement voir que les bits en mémoire sont
différents.)

sam.
Avatar
Samuel DEVULDER
Vincent Lefevre a écrit :

Sans doute. Mais si tu as des exemples concrets ca m'interresse



Le cas classique:

if (x != y)
a = b / (x - y);
else
...



Perso je fais pas confiance au x!=y pour la division, car si ca diffère
d'un pouieme sensiblement égal à la précision, le résultat de la
division contient une erreur énorme sur la valeur de "a" (plusieurs
ordres de grandeurs au dessus). Dans ce cas on aurait préféré passer par
le "else" supposé traiter le cas ou x et y sont très proches.

Par exemple pour calculer:

a(x) = (x-0.1)/(exp(x-0.1)-1)

on utilisera le développement de Taylor dans la partie "else" quand x
sera trop près de la valeur 0.1:

if(fabs(x-0.1)>seuil)
a = (x-0.1)/(exp(x-0.1) - 1);
else
a = 1 - (x-0.1)/2 + pow(x-0.1,2)/12 - pow(x-0.1,4)/720;

Mais bon à titre expérimental pourquoi en effet ne pas utiliser x!=0.1.
Il doit se passer un truc spécial autour de 0.1 qui fait que la valeur
de "a" va être assez différente de l'unité IMHO.

On a aussi x != x pour tester si x est NaN.



Ok je vois.. mais bon isnan() c'est bien aussi et surtout beaucoup plus
clair (sauf si on veut montrer qu'on est un maitre es-IEEE ;-) ).

Aussi après un appel de rint...



pour savoir si un nombre est un entier ou pas? La comparaison avec un
seuil marche bien aussi dans ce cas.

La comparaison avec seuil n'est certes pas la panacée, mais un bon truc
à utiliser si on ne veut pas se tirer une balle dans le pied par mégarde
avec le format flottant.

Après, on peut passer dans des algo avancés, où on contrôle l'erreur.
Un exemple dans le code de CRlibm:

if(logh == (logh + (logm * RNROUNDCST)))



Oui ca c'est déjà plus intéressant: j'ai entendu parler de cette
technique dans des algo style simplex. Pour ceux qui connaissent: si
l'erreur cumulée est trop grande, on recalcule la matrice à partir des
vecteurs en base. Le terme d'erreur est alors réduit de plusieurs ordres
de grandeurs. Dans le même style de maitrise de l'erreur, il y a:
http://www.inf.ethz.ch/personal/gaertner/texts/own_work/cgta_final.ps.gz

**fin de la page de pub pour la prog.linéaire**

car en fait je me dit que si on veut une équalité stricte, alors il
est plus simple de passer par un union et comparer (bit à bits) les
entiers correspondants.



Non! D'une part c'est incorrect, car les représentations ne sont pas
forcément identiques



Pourquoi donc? tous les doubles sont sérialisés en mémoire de la même
façon au format IEEE. Les 8 octets doivent être identiques si les
nombres le sont (je passe outre les subtilités de nombres non
dénormalisés, et encore cela se discute).

(encore plus s'il y a des bytes de padding, comme
avec un long double en 80 bits dans une union). D'autre part, ce serait
plus lent.



Ca n'est pas le propos. Le propos était de voir si les deux "double"s
sont identiques (sous entendu en mémoire). L'op utilise un printf %g (ou
%f, peu importe) qui n'affiche pas assez de précision comme tu
l'indiquais, mais aussi qui ne permet pas de "voir" les bits qui
diffèrent en mémoire entre le 1.7+0.1 qui est égal à 1.8 et celui qui ne
l'est pas. Et puis, pour regarder une différence en mémoire, les entiers
c'est bien aussi (mais ok moins moderne).

Des types de <stdint.h> pourraient être utilisés. Mais c'est peut-être
trop nouveau...



Oui peut-être.. normalement cela devrait être accepté vu qu'alors dans
le code on utilise plus directement "int" qui est banni.

Note: je crois que Misra-C n'est pas encore C99. Il est C95 (le standard
effectivement utilisé par les compilos du domaine), et l'on parle de C99
uniquement dans la révision 2010 du standard Misra (et encore certains
raconte 99- plutot que 95+.. bon.. oui s'ils veulent.)

sam.
Avatar
Jean-Marc Bourguet
Samuel DEVULDER writes:

Le code est tout de même faux sur la plupart des machines actuelles
(avec des long sur 64 bits)!



Hum? Je ne vois pas. D'un coté tu as 2 long int à 32 bits chacun, et de
l'autre 64bits. Ca doit rentrer.



Le code est donc:

union {long int i[2]; double d;} nb;
nb.d = 1.7 + 0.1;
printf("%08x%08xn", nb.i[0], nb.i[1]);
nb.d = x + 0.1;
printf("%08x%08xn", nb.i[0], nb.i[1]);



- x attend un unsigned int on lui passe un long, c'est non conforme

- on ne lit pas le dernier membre écrit dans l'union, à nouveau non
conforme.

A+

--
Jean-Marc
FAQ de fclc: http://www.levenez.com/lang/c/faq
Site de usenet-fr: http://www.usenet-fr.news.eu.org
Avatar
Samuel DEVULDER
Jean-Marc Bourguet a écrit :

Le code est donc:

union {long int i[2]; double d;} nb;
nb.d = 1.7 + 0.1;
printf("%08x%08xn", nb.i[0], nb.i[1]);
nb.d = x + 0.1;
printf("%08x%08xn", nb.i[0], nb.i[1]);



- x attend un unsigned int



ah? j'aurais dit un float d'après le code.

on lui passe un long, c'est non conforme



Où a tu lu qu'on écrit dans x?? Moi je conclue plutôt que ce sont tes
lunettes qui sont non conformes ;-)

Faudrait arrêter de couper les cheveux en 4 à un moment et se concentrer
sur les vrais pbs. J'ai bien précisé dans ma réponse d'origine:

(bon c'est l'idée... )

C'est à dire qu'ici je me fiche des détails qui ne sont pas important
pour l'exemple. J'aurais pu tout aussi justement écrire:

double d = 1.7 + 0.1;
long *ptr = &d;
printf("%08x%08xn" ptr[0], ptr[1]);
d = x + 0.1;
printf("%08x%08xn" ptr[0], ptr[1]);

Ca aurait le même but: un code indicatif, jouet, pour voir en quoi
1.7+0.1 et x+0.1 en mémoire ne sont pas a même chose.

sam.
Avatar
Samuel DEVULDER
Samuel DEVULDER a écrit :

long *ptr = &d;



Pour les amateurs de trétacapillotomie[*], on peut écrire:

long *ptr = (void*)&d;

Si on a peur du grand méchant Waaaaaarning dans du code jouet. Les
autres auront compris qu'ici on voulait juste récupérer l'adresse du
double pour l'examiner en mémoire au travers d'une représentation
héxadécimale.

sam.

[*] tétra = 4, capillo = cheveux, tomie = couper.
Avatar
Jean-Marc Bourguet
Samuel DEVULDER writes:

Jean-Marc Bourguet a écrit :

Le code est donc:

union {long int i[2]; double d;} nb;
nb.d = 1.7 + 0.1;
printf("%08x%08xn", nb.i[0], nb.i[1]);
nb.d = x + 0.1;
printf("%08x%08xn", nb.i[0], nb.i[1]);



- x attend un unsigned int



ah? j'aurais dit un float d'après le code.

on lui passe un long, c'est non conforme



Où a tu lu qu'on écrit dans x?? Moi je conclue plutôt que ce sont tes
lunettes qui sont non conformes ;-)



Je parlais naturellement du x spécificateur de format pour printf.
(Effectivement il devait avoir une tâche sur mes lunettes, je n'ai pas fait
attention à l'autre x).

Faudrait arrêter de couper les cheveux en 4 à un moment et se concentrer
sur les vrais pbs. J'ai bien précisé dans ma réponse d'origine:

(bon c'est l'idée... )

C'est à dire qu'ici je me fiche des détails qui ne sont pas important pour
l'exemple. J'aurais pu tout aussi justement écrire:

double d = 1.7 + 0.1;
long *ptr = &d;
printf("%08x%08xn" ptr[0], ptr[1]);
d = x + 0.1;
printf("%08x%08xn" ptr[0], ptr[1]);

Ca aurait le même but: un code indicatif, jouet, pour voir en quoi 1.7+0.1
et x+0.1 en mémoire ne sont pas a même chose.



Avec exactement les mêmes problèmes qu'on passe à printf autre chose que ce
qu'on lui indique et qu'on suppose que le compilateur ne va pas décider des
choses en supposant que d n'est jamais utilisé (ptr est un pointeur vers
long, il ne peut pas aliaser un double).

Pour éviter ces problèmes

unsigned char* ptr = (char*)&d;
for (i = 0; i < sizeof(double); ++i)
printf("%02x", ptr[i]);

si on ne dispose pas de %a.

A+

--
Jean-Marc
FAQ de fclc: http://www.levenez.com/lang/c/faq
Site de usenet-fr: http://www.usenet-fr.news.eu.org
Avatar
Samuel DEVULDER
Jean-Marc Bourguet a écrit :

Avec exactement les mêmes problèmes qu'on passe à printf autre chose que ce
qu'on lui indique et qu'on suppose que le compilateur ne va pas décider des
choses en supposant que d n'est jamais utilisé (ptr est un pointeur vers
long, il ne peut pas aliaser un double).



Oui, tu as raison. On peu supposer beaucoup de choses.. mais vu qu'on en
est à avoir un compilo sous la main (on s'éloigne de l'exemple de code
jouet pour fixer les idée du départ, mais bon), j'en connais qui, en ne
supposant pas grand chose, en optimisant pas vraiment (tcc par exemple),
refusent le code parce qu'il manque une belle virgule avant prt[0].

Pour éviter ces problèmes

unsigned char* ptr = (char*)&d;
for (i = 0; i < sizeof(double); ++i)
printf("%02x", ptr[i]);

si on ne dispose pas de %a.



Ok, c'est exact. C'est quand même bien plus lourd que les unions du
début. J'ose pas imaginer le code pour recopier les deux valeurs doubles
dans deux fois deux long et faire un xor entre les deux pour afficher
les bits qui ont bougés. Je sens que globalement je préfère le 1er code,
qui s'il n'est pas strictement juste, a le mérite de la simplicité.

En tout cas une chose est sure: à voir les messages, il y a presque plus
à dire autour de l'usage des unions pour afficher des doubles sans
passer par des pointeurs que de comprendre en quoi:
(1.7f + 0.1) != (1.7 + 0.1).

bon allez.. je crois qu'on a fait le tour et qu'on apporte plus rien à
la question d'origine. Tout est dit.

sam (l'union a semée la division apparemment, c'est paradoxal :-) )
Avatar
Vincent Belaïche
Bonjour,

Si j'ai bien compris %a est le spécificateur format de printf pour
afficher un float en base 16.

Y a-t-il un site où on peut trouver les réponses aux questions suivantes:

Question 1 : pour les doubles c'est %la ?

Question 2 : quelle est la lettre pour l'exposant en hexa
(e comme en décimal?)

Question 3 : y a-t-il un équivalent des notations fixe, scientifique, et

ingénérie (%f, %e et %g)

Question 4 : Comment écrit-on une constante immédiates float en hexa
Est-ce que 0x0.1f est équivalent à 0.0625f, 0x0.1 à

0.0625?

Question 5 : En C++ (pas en C) un ostream de gcc formate -0.0
(0.0 avec un bit de signe = 1) en `-0.0', alors que
VisualC++ le formate en `0.0'. Je ne sais pas ce que fait
printf, mais comment ça devrait être ? Et en général,
est-ce qu'un 0.0 avec un bit de signe à 1 est une valeur
valide ?


Merci d'avance,

Vincent.


Samuel DEVULDER a écrit :
Jean-Marc Bourguet a écrit :

Avec exactement les mêmes problèmes qu'on passe à printf autre chose
que ce
qu'on lui indique et qu'on suppose que le compilateur ne va pas
décider des
choses en supposant que d n'est jamais utilisé (ptr est un pointeur vers
long, il ne peut pas aliaser un double).



Oui, tu as raison. On peu supposer beaucoup de choses.. mais vu qu'on en
est à avoir un compilo sous la main (on s'éloigne de l'exemple de code
jouet pour fixer les idée du départ, mais bon), j'en connais qui, en ne
supposant pas grand chose, en optimisant pas vraiment (tcc par exemple),
refusent le code parce qu'il manque une belle virgule avant prt[0].

Pour éviter ces problèmes

unsigned char* ptr = (char*)&d;
for (i = 0; i < sizeof(double); ++i)
printf("%02x", ptr[i]);

si on ne dispose pas de %a.



Ok, c'est exact. C'est quand même bien plus lourd que les unions du
début. J'ose pas imaginer le code pour recopier les deux valeurs doubles
dans deux fois deux long et faire un xor entre les deux pour afficher
les bits qui ont bougés. Je sens que globalement je préfère le 1er code,
qui s'il n'est pas strictement juste, a le mérite de la simplicité.

En tout cas une chose est sure: à voir les messages, il y a presque plus
à dire autour de l'usage des unions pour afficher des doubles sans
passer par des pointeurs que de comprendre en quoi:
(1.7f + 0.1) != (1.7 + 0.1).

bon allez.. je crois qu'on a fait le tour et qu'on apporte plus rien à
la question d'origine. Tout est dit.

sam (l'union a semée la division apparemment, c'est paradoxal :-) )
Avatar
Samuel DEVULDER
Vincent Belaïche a écrit :
Bonjour,

Si j'ai bien compris %a est le spécificateur format de printf pour
afficher un float en base 16.



Je peux pas te répondre sur tout. Le fait est que le format %a n'est pas
référencé partout. Trop récent peut être. Perso je connaissais le %a via
la doc java
(http://java.sun.com/j2se/1.5.0/docs/api/java/util/Formatter.html), oh
sacrilège! En C je n'ai jamais utilisé, mais il a du sens pour
représenter exactement les constantes.

Y a-t-il un site où on peut trouver les réponses aux questions suivantes:

Question 1 : pour les doubles c'est %la ?



Je sais pas. J'arrive pas à trouver de truc clair la dessus. Un approche
empirique a l'air de dire que gcc accepte double et float sans soucis.
Le code asm montre cependant que le float est converti en double avant
d'être envoyé sur la pile pour le printf. Ca relativise l'intérêt de
"%a" pour regarder ce qu'on a *exactement* en mémoire car une conversion
a lieu.

On dirait que "%a" affiche tous les quartets (tranche de 4bits) du
double sauf ceux de poids faibles ne contenant que du zero. C'est
l'équivalent d'une représentation décimale où l'on affiche pas les zero
terminaux.

Question 2 : quelle est la lettre pour l'exposant en hexa
(e comme en décimal?)



Le format de sorti est assez clair. Il commence toujours par 0x, puis
est suivi de la partie entiere (0 ou 1 apparement), suivi
optionnellement d'un point suivi par la partie "hexadécimale"
fractionnaire, le tout *toujours* suivi par p (pour power, exposant)
suivi par un entier représantant l'exposant non biaisée. Le 'p' est
important pour différencier les constantes float hexa des constantes
entieres hexa.


Question 3 : y a-t-il un équivalent des notations fixe, scientifique, et
ingénérie (%f, %e et %g)



Je sais pas.

Question 4 : Comment écrit-on une constante immédiates float en hexa
Est-ce que 0x0.1f est équivalent à 0.0625f, 0x0.1 à
0.0625?



Apparemment il faut toujours le 'p'. Le format ne semble pas super
libre. La partie entière est 0 (pour 0.0 exclusivement) ou 1 (pour les
autres nombres), suivi de la partie fractionnaire précédée par un '.'
(si non nulle).

Donc 1/16 (ton exemple) s'écrit 0x1p-4 = 1/2^4 car il n'y a pas de
partie fractionnaire.

Le float 0.1f s'écrit lui 0x1.99999ap-4 = 0x199999a/0x1000000 * 2^-4 =
26843546 / 16777216 / 16 = 1,6000000238... / 16 = 0,10000000149... On
voit bien que 0.1f n'est pas égal en mémoire à 0.1.

Question 5 : En C++ (pas en C) un ostream de gcc formate -0.0
(0.0 avec un bit de signe = 1) en `-0.0', alors que
VisualC++ le formate en `0.0'. Je ne sais pas ce que fait
printf, mais comment ça devrait être ? Et en général,
est-ce qu'un 0.0 avec un bit de signe à 1 est une valeur
valide ?




Il faut croire que VC++ estime que -0.0 est 0.0 c'est pareil (ce qui
n'est pas faux) et l'affiche de façon normalisée.

sam.
1 2 3 4 5