2009-08-26 6 views
8

Je ne peux que supposer que c'est un bug. La première assertion passe tandis que la seconde échoue:gcc bogue de précision?

double sum_1 = 4.0 + 6.3; 
assert(sum_1 == 4.0 + 6.3); 

double t1 = 4.0, t2 = 6.3; 

double sum_2 = t1 + t2; 
assert(sum_2 == t1 + t2); 

Si ce n'est pas un bogue, pourquoi?

+0

Vous pouvez également consulter: - [http://stackoverflow.com/questions/21265/comparing-ieee-floats-and-doubles-for-equality](http://stackoverflow.com/questions/21265/ comparaison-ieee-floats-et-doubles-for-égalité) - [http://stackoverflow.com/questions/17333/most-effective-way-for-float-and-double-comparison](http://stackoverflow .com/questions/17333/le plus efficace-pour-float-et-double-comparaison) - [http://stackoverflow.com/questions/713763/strange-results-with-floating-point-comparison] (http://stackoverflow.com/questions/713763/strange-results-with-floating-point-comparison) – pingw33n

Répondre

12

C'est quelque chose qui m'a mordu aussi.

Oui, les nombres à virgule flottante ne devraient jamais être comparés pour l'égalité en raison d'une erreur d'arrondi, et vous le saviez probablement.

Mais dans ce cas, vous calculez t1+t2, puis vous le calculez à nouveau. Sûrement qui doit produire un résultat identique?

Voici ce qui se passe probablement. Je parie que vous exécutez ceci sur un processeur x86, correct? La FPU x86 utilise 80 bits pour ses registres internes, mais les valeurs en mémoire sont stockées en tant que doubles de 64 bits.

Donc, t1+t2 est d'abord calculé avec 80 bits de précision, puis - je présume - stocké dans la mémoire en sum_2 avec 64 bits de précision - et certains arrondis se produit. Pour l'assert, il est chargé dans un registre à virgule flottante, et t1+t2 est de nouveau calculé, toujours avec 80 bits de précision. Donc maintenant vous comparez sum_2, qui était auparavant arrondi à une valeur à virgule flottante de 64 bits, avec t1+t2, qui a été calculé avec une précision plus élevée (80 bits) - et c'est pourquoi les valeurs ne sont pas exactement identiques.

Modifier Alors, pourquoi le premier test est-il réussi? Dans ce cas, le compilateur évalue probablement 4.0+6.3 au moment de la compilation et le stocke comme une quantité de 64 bits - à la fois pour l'affectation et pour l'affirmation.Des valeurs identiques sont donc comparées, et l'affirmation passe.

Second Edition Voici le code assembleur généré pour la deuxième partie du code (gcc, x86), avec des commentaires - à peu près suit le scénario décrit ci-dessus:

// t1 = 4.0 
fldl LC3 
fstpl -16(%ebp) 

// t2 = 6.3 
fldl LC4 
fstpl -24(%ebp) 

// sum_2 = t1+t2 
fldl -16(%ebp) 
faddl -24(%ebp) 
fstpl -32(%ebp) 

// Compute t1+t2 again 
fldl -16(%ebp) 
faddl -24(%ebp) 

// Load sum_2 from memory and compare 
fldl -32(%ebp) 
fxch %st(1) 
fucompp 

note intéressante: Cette a été compilé sans optimisation. Lorsqu'il est compilé avec -O3, le compilateur optimise tous les du code.

+0

Je ne pense pas que ce soit encore. Le fait est que '4.0 + 6.3' est une expression qui est constamment pliée par le compilateur à 10.3. Donc le premier assert devient équivalent à 'assert (10.3 == 10.3)' qui passe trivialement. Dans le deuxième test, il prend en fait 4.0, le met dans un double, prend 6.3, le met dans un double (perdre un peu de précision), les ajoute ensemble, et compare * ça * à la constante 10.3, qui échoue parce qu'il est différent de 2^-70 ou plus. :) – hobbs

+0

Je ne suis pas convaincu ... si le compilateur peut optimiser t1 + t2 à 10.3 dans l'instruction assert, pourquoi ne peut-il pas faire la même optimisation dans l'affectation à sum_2? –

+0

En regardant la liste, vous m'avez. :) Pauvre deviner de ma part, je suppose. – hobbs

13

Vous comparez des nombres à virgule flottante. Ne faites pas cela, les nombres à virgule flottante ont une erreur inhérente de précision dans certaines circonstances. Au lieu de cela, prenez la valeur absolue de la différence des deux valeurs et affirment que la valeur est inférieure à un petit nombre (epsilon).

void CompareFloats(double d1, double d2, double epsilon) 
{ 
    assert(abs(d1 - d2) < epsilon); 
} 

Ceci n'a rien à voir avec le compilateur et tout ce qui concerne la façon dont les nombres à virgule flottante sont implémentés. voici la spécification IEEE:

http://www.eecs.berkeley.edu/~wkahan/ieee754status/IEEE754.PDF

+4

Ce que Ed appelle l'erreur de précision est une caractéristique de l'arithmétique en virgule flottante. Il n'y a pas moyen de contourner cela, ce n'est pas un bug, pas une mise en œuvre négligente de la part des écrivains gcc, c'est la façon dont les ordinateurs font de l'arithmétique sur les fractions. Google pour «Ce que tout scientifique informatique devrait savoir sur l'arithmétique en virgule flottante» et de mettre ses leçons à cœur. –

+0

Oui, j'ai ajouté un lien vers la spécification. –

+1

Google a aussi ce "bug": http://www.google.com/search?hl=fr&source=hp&q=999999999999999+-+999999999999998&btnG=Google+Search&cts=1251328513752&aq=f&oq=&aqi= – llamaoo7

1

La comparaison des nombres double précision sont intrinsèquement inexactes. Par exemple, vous pouvez souvent trouver 0.0 == 0.0 renvoyant false. Cela est dû à la façon dont la FPU stocke et suit les numéros.

Wikipedia says:

test pour l'égalité est problématique. Deux séquences de calcul mathématiquement égales peuvent produire différentes valeurs à virgule flottante.

Vous aurez besoin d'utiliser un delta pour donner une tolérance à vos comparaisons, plutôt qu'une valeur exacte.

2

Il se peut que dans l'un des cas, vous finissiez par comparer un registre interne de 64 bits à un registre interne de 80 bits. Il peut être éclairant de regarder les instructions d'assemblage que GCC émet pour les deux cas ...

3

J'ai dupliqué votre problème sur mon Intel Core 2 Duo, et j'ai regardé le code d'assemblage. Voici ce qui se passe: lorsque votre compilateur évalue t1 + t2, il ne

load t1 into an 80-bit register 
load t2 into an 80-bit register 
compute the 80-bit sum 

Quand il stocke dans sum_2 il ne

round the 80-bit sum to a 64-bit number and store it 

Ensuite, la comparaison == compare la somme de 80 bits à une somme de 64 bits, et ils sont différents, principalement parce que la partie fractionnaire 0.3 ne peut pas être représentée exactement en utilisant un nombre à virgule flottante binaire, donc vous comparez une 'décimale répétitive' (en fait répétitive binaire) qui a été tronquée à deux longueurs différentes. Ce qui est vraiment irritant, c'est que si vous compiliez avec gcc -O1 ou gcc -O2, gcc fait la mauvaise arithmétique au moment de la compilation, et le problème disparaît. Peut-être que c'est OK selon la norme, mais c'est juste une raison de plus que gcc n'est pas mon compilateur préféré.


P.S. Quand je dis que == compare une somme de 80 bits avec une somme de 64 bits, bien sûr, je veux dire vraiment qu'il compare la version étendue de la somme de 64 bits. Vous pourriez bien penser

sum_2 == t1 + t2 

décide de

extend(sum_2) == extend(t1) + extend(t2) 

et

sum_2 = t1 + t2 

décide de

sum_2 = round(extend(t1) + extend(t2)) 

Bienvenue dans le monde merveilleux de la virgule flottante!

3

Lorsque l'on compare les nombres à virgule flottante pour la proximité que vous voulez généralement de mesurer leur différence relative, qui est définie comme

if (abs(x) != 0 || abs(y) != 0) 
    rel_diff (x, y) = abs((x - y)/max(abs(x),abs(y)) 
else 
    rel_diff(x,y) = max(abs(x),abs(y)) 

Par exemple,

rel_diff(1.12345, 1.12367) = 0.000195787019 
rel_diff(112345.0, 112367.0) = 0.000195787019 
rel_diff(112345E100, 112367E100) = 0.000195787019 

L'idée est de mesurer le nombre de leader chiffres significatifs les nombres ont en commun; Si vous prenez le -log10 de 0.000195787019, vous obtenez 3.70821611, ce qui correspond au nombre de chiffres de base de 10 chiffres que tous les exemples ont en commun.

Si vous devez déterminer si deux nombres à virgule flottante sont égaux, vous devez faire quelque chose comme

if (rel_diff(x,y) < error_factor * machine_epsilon()) then 
    print "equal\n"; 

où la machine epsilon est le plus petit nombre qui peut être tenu dans la mantisse du matériel à virgule flottante utilisé. La plupart des langages informatiques ont un appel de fonction pour obtenir cette valeur. error_factor devrait être basé sur le nombre de chiffres significatifs que vous pensez être consommés par les erreurs d'arrondi (et autres) dans les calculs des nombres x et y. Par exemple, si je savais que x et y étaient le résultat d'environ 1000 sommations et ne connaissaient aucune limite sur les nombres étant additionnés, je placerais error_factor à environ 100.

Essayé d'ajouter ces liens, mais couldn ' t puisque c'est mon premier post:

  • en.wikipedia.org/wiki/Relative_difference
  • en.wikipedia.org/wiki/Machine_epsilon
  • en.wikipedia.org/wiki/Significand (mantisse)
  • en.wikipedia.org/wiki/Rounding_error