Je vais essayer de fournir des exemples plus ou moins réels de tests légitimes, significatifs et utiles pour l'égalité des flottants.
#include <stdio.h>
#include <math.h>
/* let's try to numerically solve a simple equation F(x)=0 */
double F(double x) {
return 2*cos(x) - pow(1.2, x);
}
/* I'll use a well-known, simple&slow but extremely smart method to do this */
double bisection(double range_start, double range_end) {
double a = range_start;
double d = range_end - range_start;
int counter = 0;
while(a != a+d) // <-- WHOA!!
{
d /= 2.0;
if(F(a)*F(a+d) > 0) /* test for same sign */
a = a+d;
++counter;
}
printf("%d iterations done\n", counter);
return a;
}
int main() {
/* we must be sure that the root can be found in [0.0, 2.0] */
printf("F(0.0)=%.17f, F(2.0)=%.17f\n", F(0.0), F(2.0));
double x = bisection(0.0, 2.0);
printf("the root is near %.17f, F(%.17f)=%.17f\n", x, x, F(x));
}
Je préfère ne pas expliquer le méthode de bissection utilisé lui-même, mais insiste sur la condition d'arrêt. Il a exactement la forme discutée : (a == a+d)
où les deux côtés sont des flotteurs : a
est notre approximation actuelle de la racine de l'équation, et d
est notre précision actuelle. Étant donné la condition préalable de l'algorithme - qu'il y a doit soit une racine entre range_start
et range_end
- nous garantie à chaque itération où la Racine reste entre a
et a+d
tandis que d
est divisé par deux à chaque étape, ce qui réduit les limites.
Et ensuite, après un certain nombre d'itérations, d
devient si petit que lors de l'addition avec a
il est arrondi à zéro ! C'est-à-dire, a+d
s'avère être plus près à a
puis à tout autre flotteur et la FPU l'arrondit donc à la valeur la plus proche : à l'unité de mesure de l'unité de mesure de l'énergie. a
même. Ceci peut être facilement illustré par un calcul sur une hypothétique machine à calculer ; qu'elle ait une mantisse décimale à 4 chiffres et une large gamme d'exposants. Alors quel résultat la machine devrait donner à 2.131e+02 + 7.000e-3
? La réponse exacte est 213.107
mais notre machine ne peut pas représenter un tel nombre, elle doit l'arrondir. Et 213.107
est beaucoup plus proche de 213.1
que de 213.2
- Le résultat arrondi est donc 2.131e+02
- le petit sommand a disparu, arrondi à zéro. Exactement la même chose est garanti à se produire à une certaine itération de notre algorithme - et à ce moment-là, nous ne pouvons plus continuer. Nous avons trouvé la Racine avec la plus grande précision possible.
La conclusion édifiante est, apparemment, que les flotteurs sont délicats. Ils ressemblent tellement réel que tout programmeur est tenté de les considérer comme des nombres réels. Mais ils ne le sont pas. Ils ont leur propre comportement, qui rappelle légèrement celui de réel mais pas tout à fait la même. Il faut faire très attention à eux, surtout lorsqu'on compare l'égalité.
Mise à jour
En réexaminant la réponse après un certain temps, j'ai également remarqué un fait intéressant : dans l'algorithme ci-dessus on ne peut pas utiliser réellement "un petit nombre" dans la condition d'arrêt. Pour n'importe quel choix du nombre, il y aura des entrées qui considéreront que votre choix trop grand ce qui entraîne une perte de précision, et il y aura des entrées qui détermineront votre choix. trop petit ce qui entraîne des itérations excessives, voire l'entrée dans une boucle infinie. Une discussion détaillée suit.
Vous savez peut-être déjà que le calcul ne connaît pas la notion de "petit nombre" : pour tout nombre réel, vous pouvez facilement en trouver une infinité de plus petits encore. Le problème est que l'un de ces nombres "encore plus petits" pourrait être ce que nous cherchons réellement ; il pourrait être une racine de notre équation. Pire encore, pour différentes équations, il peut y avoir distinct racines (par exemple 2.51e-8
et 1.38e-8
), les deux dont la valeur sera approximée par le même si notre condition d'arrêt ressemblait à d < 1e-6
. Quel que soit le "petit nombre" que vous choisirez, de nombreuses racines qui auraient été trouvées correctement avec une précision maximale avec a == a+d
La condition d'arrêt sera gâchée par le fait que l'"epsilon" soit trop grand .
Il est vrai cependant que dans les nombres à virgule flottante, l'exposant a une portée limitée, de sorte que vous pouvez effectivement trouver le plus petit nombre positif non nul de FP (par ex. 1e-45
denorm pour IEEE 754 single precision FP). Mais c'est inutile ! while (d < 1e-45) {...}
tournera en boucle à l'infini, en supposant qu'il s'agisse d'une simple précision (positif non nul). d
.
En mettant de côté les cas extrêmes pathologiques, tout choix du "petit nombre" dans le d < eps
La condition d'arrêt sera trop petit pour de nombreuses équations. Dans les équations où la racine a un exposant suffisamment élevé, le résultat de la soustraction de deux mantisses ne différant que par le chiffre le moins significatif dépassera facilement notre "epsilon". Par exemple, avec des mantisses à 6 chiffres 7.00023e+8 - 7.00022e+8 = 0.00001e+8 = 1.00000e+3 = 1000
Ce qui signifie que la plus petite différence possible entre des nombres avec un exposant +8 et une mantisse à 5 chiffres est... 1000 ! Ce qui ne rentrera jamais dans, disons, 1e-4
. Pour ces nombres à exposant (relativement) élevé, nous n'avons tout simplement pas assez de précision pour voir un jour une différence de 1e-4
.
Mon implémentation ci-dessus a également pris en compte ce dernier problème, et vous pouvez voir que d
est divisé par deux à chaque étape, au lieu d'être recalculé comme une différence de (éventuellement énorme en exposant) a
et b
. Donc si nous changeons la condition d'arrêt en d < eps
l'algorithme ne sera pas bloqué dans une boucle infinie avec des racines énormes (il pourrait très bien l'être avec l'option (b-a) < eps
), mais effectuera toujours des itérations inutiles pendant le rétrécissement. d
en dessous de la précision de a
.
Ce type de raisonnement peut sembler trop théorique et inutilement profond, mais il a pour but d'illustrer une fois de plus l'astuce des flotteurs. Il faut faire très attention à leur précision finie lorsqu'on écrit des opérateurs arithmétiques autour d'eux.
1 votes
J'ai toujours pensé que
foo == bar
maisbar != pi
:)7 votes
Qui a rétrogradé ça ? C'est une bonne question.
0 votes
Une espèce étroitement liée à lire absolument blog en profondeur sur le sujet : randomascii.wordpress.com/2013/07/16/floating-point-determinism