comparaison de flottants

Le
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", 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
Vidéos High-Tech et Jeu Vidéo
Téléchargements
Vos réponses Page 1 / 21
Gagnez chaque mois un abonnement Premium avec GNT : Inscrivez-vous !
Trier par : date / pertinence
Jean-Marc Bourguet
Le #21458041
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;



Toutes ces valeurs sont des doubles, le calcul se fait en double ou avec
une précision plus grande.

return ((x + 0.1) == 1.8) ? 1 : 0;



x est un float qui est converti en un double avant que le calcul se fasse
en double (ou avec une précision plus grande).

Il est donc assez normal que les expressions x+0.1 et 1.7+0.1 ne soient pas
égale.

Savoir si une d'entre elle est égale à 1.8 est plus délicat. Tu aurais tout
aussi bien te retrouver avec deux fois une inégalité. (Mais si j'ai bonne
mémoire, les versions récentes de gcc effectuent le calcul sur les
expressions constantes avec une précision infinie.)

(En passant, les formats de flottant généralement utilisés ne permettent
pas de représenter des valeurs comme 0.1, 1.7 et 1.8 exactement, pas plus
qu'un format décimal permet de représenter 1/11, 17/11 ou 18/11
exactement).
Pourtant, le programme

#include int main(void) {
float x;
x = 1.7;
printf("%f et %fn", 1.7 + 0.1, x + 0.1);
}

affiche :
1.800000 et 1.800000



Tu n'affiches pas assez de chiffres pour voir la différence.

#include int main(void) {
float x;
x = 1.7;
printf("%.20f et %.20fn", 1.7 + 0.1, x + 0.1);
}

donne
1.80000000000000004441 et 1.80000004768371590913

Si tu remplaces float par double, tu as deux fois la première valeur.

A+

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

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



Attention, le 1.7 de x est un float ici (23bits de précision), et le 0.1
(et le 1.8) de l'addition (ou de la comparaison) est un double (52bits
de précision).. Bref l'un a des bits significatifs en trop par rapport à
l'autre. Le test ne veut rien dire. C'est l'erreur classique que l'on
trouve dans les exos de physiques quand les élèves sortent un résultat
avec 5 chiffres significatifs alors que les données du problème n'en ont
que 2.. les chiffres en trop n'ont pas de sens. Moralement les nombre de
la comparaisons devraient être vu avec tout juste la précision d'un
float pour avoir le moindre sens.

Par ailleurs dans ton 1er test avec des constantes, tout est double, le
test a déjà plus de sens, mais il est probable que l'optimiseur minimal
remplace 1.7 + 0.1 par 1.8 directement, d'où égalité stricte bit à bits
(sans compter que si c'est stocké dans des registres, il y a des bits
supplémentaires par rapport au formats IEEE double).

En outre il faut savoir que le décimal 0.1 ne se représente pas
*exactement* en binaire. Ce problème se produit avec tout nombre décimal
qui n'est pas une puissance de deux. Aussi, il vaut mieux privilégier
les puissances de deux dans ses constantes.. quitte à renormaliser le
calcul via une seule division/multiplication tout à la fin de l'algo si
c'est possible.


Pourtant, le programme

#include int main(void) {
float x;
x = 1.7;
printf("%f et %fn", 1.7 + 0.1, x + 0.1);
}

affiche :
1.800000 et 1.800000



Ca ne veut rien dire.. Le printf effectue une conversion des float en
double et un arrondi pour l'affichage. Bref, affiche plutot les nombres
en binaires/hexa si tu veux voir si quelque chose diffère au niveau des
bits:

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]);

(bon c'est l'idée... attention pour l'interprétation des valeur hexa les
big-endian/little-endian sont de la partie si tu veux y retrouver
l'exposant, la mantisse, etc).

L'usage des doubles/float n'est pas simple si on doit se préoccupper des
erreurs. Aussi il est souvent recommandé de ne pas utiliser d'égalité
stricte, mais d'introduire une tolérance: a==b devenant fabs(a-b)<=seuil.

sam.
Antoine Leca
Le #21459781
Emmanuel écrivit :
float x = 1.7;
printf("%f et %fn", 1.7 + 0.1, x + 0.1);
affiche :
1.800000 et 1.800000

Apparremment, 1.7+0.1 et x+0.1 sont égaux ;



Seulement apparemment, et seulement dans le cadre de ton test.

j'imagine qu'il s'agit d'un problème de représentation des flottants



Voui

(et une explication, si possible).



Les nombres flottants sont stockées sur ta machine en base 2. En base 2,
1/2 est un nombre binaire, à savoir 0,1 (« binaire » par analogie aux
nombres décimaux en base 10, c'est-à-dire élément de ID, ou encore qui
ont une représentation avec un nombre fini de chiffres);
1/3 ou 1/11 (pour faire plaisir à JM) ne sont ni binaires ni décimaux
(nombre infini de décimales);
1/5 et donc 1/10 sont différents, ils sont décimaux (resp. 0,2 et 0,1)
mais ne sont pas binaires (nombre infini de chiffres).

Et faire des opérations sur des suites infinies de chiffres amènent des
erreurs (dites de précision ou d'arrondi); <MODE PAPY>si je peux me
permettre un conseil au débutant, c'est une bonne raison pour éviter de
se servir des nombres flottants, surtout si l'on cherche à avoir des «
égalités » ou plus généralement des résultats conformes aux
représentations visuelles simples; il vaut mieux utiliser des entiers
avec un facteur de conversion: par exemple en comptabilité on utilisera
des centimes stockés dans des entiers</MODE>.

Jusque là, c'est simple.


Pour l'explication réelle, c'est un problème de taille de motif: la
fraction 1/5 en base 2 a un motif de 4 bits (1100); en comparaison, 1/3
en base a un motif de 2 bits (10), et un seul en base 10 (3); 1/11 en
base 10 a un motif de 2 chiffres (90); etc.
Pour que deux représentations infinies soient vues comme égales, il faut
(en général) que la différence en nombre de bits des mantisses (53-24
dans le cas qui t'importe) soit un multiple de taille de motif; ce n'est
pas le cas ici.


Antoine
Samuel DEVULDER
Le #21466661
Samuel DEVULDER a écrit :

L'usage des doubles/float n'est pas simple si on doit se préoccupper des
erreurs. Aussi il est souvent recommandé de ne pas utiliser d'égalité
stricte, mais d'introduire une tolérance: a==b devenant fabs(a-b)<=seuil.



A ce propos, les standard de codages industriels tel Misra-c
(http://www.misra.org.uk/) interdisent l'utilisation de "==" et "!=" sur
les floats.

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

sam.
Vincent Lefevre
Le #21469411
Dans l'article Samuel DEVULDER écrit:

Emmanuel a écrit :

> 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

Attention, le 1.7 de x est un float ici (23bits de précision), et le 0.1
(et le 1.8) de l'addition (ou de la comparaison) est un double (52bits
de précision).



float = 24 bits de précision
double = 53 bits de précision

(à cause du bit implicite). Le 1.7 est un double, converti en float;
c'est différent de 1.7f (ici, le résultat est le même, mais il peut
y avoir des différences). Dans la pratique, cela peut être encore
plus complexe.

Bref l'un a des bits significatifs en trop par rapport à l'autre. Le
test ne veut rien dire.



Noter que le véritable problème ne vient pas de la différence de
précision, mais des erreurs d'arrondi, notamment des conversions
base 10 -> base 2.

C'est l'erreur classique que l'on trouve dans les exos de physiques
quand les élèves sortent un résultat avec 5 chiffres significatifs
alors que les données du problème n'en ont que 2.. les chiffres en
trop n'ont pas de sens.



sauf si on veut pouvoir relire le résultat pour de calculs ultérieurs
et éviter des erreurs d'arrondi importantes supplémentaires.

Par ailleurs dans ton 1er test avec des constantes, tout est double, le
test a déjà plus de sens, mais il est probable que l'optimiseur minimal
remplace 1.7 + 0.1 par 1.8 directement, d'où égalité stricte bit à bits
(sans compter que si c'est stocké dans des registres, il y a des bits
supplémentaires par rapport au formats IEEE double).



Ça dépend du processeur et de l'unité flottante (e.g. sur x86, FPU x87
vs SSE).

Voir http://www.vinc17.net/research/extended.fr.html

En outre il faut savoir que le décimal 0.1 ne se représente pas
*exactement* en binaire.



Voilà, il fallait surtout le dire plus tôt.

Ce problème se produit avec tout nombre décimal qui n'est pas une
puissance de deux.



Non. Pas de problème avec 8.5, qui n'est pas une puissance de 2. :)

Aussi, il vaut mieux privilégier les puissances de deux dans ses
constantes.. quitte à renormaliser le calcul via une seule
division/multiplication tout à la fin de l'algo si c'est possible.



?

> Pourtant, le programme
>
> #include > int main(void) {
> float x;
> x = 1.7;
> printf("%f et %fn", 1.7 + 0.1, x + 0.1);
> }
>
> affiche :
> 1.800000 et 1.800000

Ca ne veut rien dire.. Le printf effectue une conversion des float en
double et un arrondi pour l'affichage.



C'est surtout que %f affiche seulement 6 chiffres après la virgule
(par défaut).

Bref, affiche plutot les nombres en binaires/hexa si tu veux voir si
quelque chose diffère au niveau des bits:

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]);



Beurk. Utiliser %a (ISO C99).

--
Vincent Lefèvre 100% accessible validated (X)HTML - Blog: Work: CR INRIA - computer arithmetic / Arénaire project (LIP, ENS-Lyon)
Vincent Lefevre
Le #21469401
Dans l'article Samuel DEVULDER écrit:

A ce propos, les standard de codages industriels tel Misra-c
(http://www.misra.org.uk/) interdisent l'utilisation de "==" et "!=" sur
les floats.

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...
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. */

--
Vincent Lefèvre 100% accessible validated (X)HTML - Blog: Work: CR INRIA - computer arithmetic / Arénaire project (LIP, ENS-Lyon)
Samuel DEVULDER
Le #21469901
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 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. 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.

Par exemple le float est parfois dans un registre (non IEEE) et en ram
(au format IEEE) et du coup quand on écrit:

float *f = ...;

*f = <une valeur>;
int same = <meme valeur> == *f;

on peut se retrouver que "same" soit faux parce que la valeur utilisée
dans la comparaison "same" est celle d'un registre avec des bits en plus
par rapport à la valeur stockée en mémoire pour f.

A noter: dans le standard Misra, il est même interdit d'utiliser des
float dans des union. Il m'est avis que c'est une construction C
intrinsèquement suspecte qu'on ne devrait pas utiliser "normalement".

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.

sam.
Samuel DEVULDER
Le #21469931
Vincent Lefevre a écrit :

Non. Pas de problème avec 8.5, qui n'est pas une puissance de 2. :)



Oui.. mais presque.. Tu as compris ce que je voulais dire.. tout nombre
qui n'est pas de la forme "entier divisé par une puissance de deux". Et
même là encore la description est incomplète car l'entier et la
puissance de deux sont bornés.

Des fois l'abondance de détails nuit. Autant garder une description pas
vraiment exacte, mais qui donne l'intuition plutôt que de rentrer dans
des détails qui embrouillent. Mais tu as raison, j'ai été trop vite avec
la simplification "puissance de deux".

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.

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.

sam.
Antoine Leca
Le #21472601
Samuel DEVULDER écrivit :
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.



C'est exact. C'est un gros défaut de la norme C de 89 : on peut
interpréter (« parser ») 1. comme 9 * 1/9, et par la suite on manipule
0.999999, qui s'arrondit en 0 (la conversion flottant vers entier en C
spécifie arrondi par défaut).

Cependant, celui qui essaye de vendre un tel compilateur (pour autre
chose que la DS9000) va faire un flop commercial retentissant. ÀMHA.


Antoine
Vincent Lefevre
Le #21472691
Dans l'article 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.

> 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)! 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.

--
Vincent Lefèvre 100% accessible validated (X)HTML - Blog: Work: CR INRIA - computer arithmetic / Arénaire project (LIP, ENS-Lyon)
Publicité
Poster une réponse
Anonyme