53 votes

La virgule flottante == est-elle toujours acceptable ?

Aujourd'hui, je suis tombé sur le logiciel d'un tiers que nous utilisons et dans leur code d'exemple, il y avait quelque chose de ce genre :

// Defined in somewhere.h
static const double BAR = 3.14;

// Code elsewhere.cpp
void foo(double d)
{
    if (d == BAR)
        ...
}

Je suis conscient du problème des points flottants et de leur représentation, mais je me suis demandé s'il y avait des cas où float == float serait bien ? Je ne demande pas quand il pourrait mais quand cela a du sens et fonctionne.

De même, qu'en est-il d'un appel comme foo(BAR) ? La comparaison sera-t-elle toujours égale puisqu'ils utilisent tous les deux la même technologie ? static const BAR ?

1 votes

J'ai toujours pensé que foo == bar mais bar != 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

37voto

Martin Beckett Points 60406

Oui, vous avez la garantie que les nombres entiers, y compris 0,0, sont comparables à ==.

Bien sûr, vous devez faire attention à la façon dont vous obtenez le nombre entier en premier lieu, l'affectation est sûre mais le résultat de tout calcul est suspect.

ps il y a un ensemble de nombres réels qui ont une reproduction parfaite en tant que flottant (pensez à 1/2, 1/4 1/8 etc) mais vous ne savez probablement pas à l'avance que vous avez un de ces nombres.

Juste pour clarifier. La norme IEEE 754 garantit que les représentations flottantes des entiers (nombres entiers) dans un intervalle sont exactes.

float a=1.0;
float b=1.0;
a==b  // true

Mais vous devez faire attention à la façon dont vous obtenez les chiffres entiers.

float a=1.0/3.0;
a*3.0 == 1.0  // not true !!

0 votes

+1 : Votre réponse est presque parfaite - vous avez besoin de "Of course" au lieu de "of course" ;)

9 votes

Pour être juste, les garanties et le comportement des nombres entiers ne sont pas différents de ceux des autres valeurs.

1 votes

@Martin Beckett : pouvez-vous développer cela avec des nombres entiers et 0.0 s'il vous plaît ? Je ne demande pas si cela fonctionnera parfois, mais plutôt si cela fonctionne (tout le temps et de manière logique).

34voto

Cameron Skinner Points 19987

Il y a deux façons de répondre à cette question :

  1. Y a-t-il des cas où float == float donne le résultat correct ?
  2. Y a-t-il des cas où float == float est un codage acceptable ?

La réponse à (1) est : Oui, parfois. Mais cela va être fragile, ce qui conduit à la réponse à (2) : Non. Ne faites pas ça. Vous risquez d'avoir des bugs bizarres à l'avenir.

Comme pour un appel de la forme foo(BAR) : Dans ce cas particulier, la comparaison retournera vrai, mais lorsque vous écrivez foo vous ne savez pas (et ne devriez pas dépendre) de la façon dont il est appelé. Par exemple, appeler foo(BAR) sera bien, mais foo(BAR * 2.0 / 2.0) (ou même peut-être foo(BAR * 1.0) selon le degré d'optimisation du compilateur) sera cassé. Vous ne devriez pas compter sur le fait que l'appelant n'effectue aucune opération arithmétique !

Pour faire court, même si a == b peut fonctionner dans certains cas, mais il ne faut pas s'y fier. Même si vous pouvez garantir la sémantique des appels aujourd'hui, vous ne pourrez peut-être pas le faire la semaine prochaine. == .

A mon avis, float == float n'est jamais* acceptable parce qu'elle est pratiquement impossible à maintenir.

*Pour les petites valeurs de jamais.

3 votes

En fait, tout ce qui concerne la virgule flottante est tout à fait standard et n'est pas susceptible de changer.

2 votes

@Alexandre : Je voulais dire qu'aujourd'hui l'appelant utilise foo(BAR) mais demain, ils pourraient le changer en foo(BAR * 1.0)

0 votes

@Cameron : merci pour l'édition. C'est très bien expliqué. Je me pose toujours la question car dans le cas que j'ai vu aujourd'hui dans une librairie tierce, ils utilisaient un nombre magique statique constant qui avait une signification spéciale et leur exemple indiquait que l'on devait vérifier cette valeur spéciale comme value == MAGIC_NUMBER. Je me demande même si ce n'est pas considéré comme une bonne programmation si cela fonctionne dans leur cas. Et si la valeur comparée à la constante est sous leur contrôle et qu'ils l'assignent toujours à MAGIC_NUMBER avant d'appeler le code client depuis leur code ? Comprenez-vous ce que je veux dire ?

14voto

sleske Points 29978

Les autres réponses expliquent assez bien pourquoi utiliser == pour les nombres à virgule flottante est dangereux. Je viens de trouver un exemple qui illustre assez bien ces dangers, je crois.

Sur la plate-forme x86, vous pouvez obtenir des résultats bizarres en virgule flottante pour certains calculs, à savoir pas en raison de problèmes d'arrondi inhérents aux calculs que vous effectuez. Ce simple programme en C affiche parfois "erreur" :

#include <stdio.h>

void test(double x, double y)
{
  const double y2 = x + 1.0;
  if (y != y2)
    printf("error\n");
}

void main()
{
  const double x = .012;
  const double y = x + 1.0;

  test(x, y);
}

Le programme calcule essentiellement

x = 0.012 + 1.0;
y = 0.012 + 1.0;

(uniquement réparti sur deux fonctions et avec des variables intermédiaires), mais la comparaison peut encore donner des résultats faux !

La raison en est que sur la plate-forme x86, les programmes utilisent généralement l'option x87 FPU pour les calculs en virgule flottante. Le x87 calcule en interne avec une précision supérieure à celle des calculateurs ordinaires. double donc double les valeurs doivent être arrondies lorsqu'elles sont stockées en mémoire. Cela signifie qu'un aller-retour x87 -> RAM -> x87 perd en précision, et donc que les résultats des calculs diffèrent selon que les résultats intermédiaires passent par la RAM ou qu'ils restent tous dans les registres de la FPU. Il s'agit bien entendu d'une décision du compilateur, de sorte que le bogue ne se manifeste que pour certains compilateurs et paramètres d'optimisation :-(.

Pour plus de détails, voir le bogue GCC : http://gcc.gnu.org/bugzilla/show_bug.cgi?id=323

Plutôt effrayant...

Note supplémentaire :

Les bogues de ce type sont généralement assez délicats à déboguer, car les différentes valeurs deviennent identiques une fois en RAM.

Si, par exemple, vous étendez le programme ci-dessus pour imprimer les modèles de bits de l'adresse suivante y et y2 juste après les avoir comparés, vous obtiendrez la même valeur . Pour imprimer la valeur, elle doit être chargée dans la RAM pour être transmise à une fonction d'impression telle que printf et cela fera disparaître la différence...

0 votes

En fait, je n'ai pas vu de compilateur récemment (au cours des dix dernières années) qui utilise le FPU x87 pour des calculs en simple et double précision.

0 votes

@gnasher729 : Alors qu'est-ce qu'il utilise ? Calculer toutes les opérations de FP dans le logiciel serait trop lent, n'est-ce pas ? Y a-t-il une autre FPU dans les CPU modernes ?

0 votes

Le bogue 323 est un bogue . Il a été corrigé il y a plusieurs versions (utilisez -std=c99 pour obtenir FLT_EVAL_METHOD==2 sémantique avec un GCC moderne). Ou, pour contourner le problème, utilisez simplement SSE2 : il est disponible depuis dix ans maintenant.

8voto

DigitalRoss Points 80400

Parfait pour les valeurs intégrales, même dans les formats à virgule flottante

Mais la réponse courte est : "Non, n'utilisez pas ==."

Ironiquement, le format à virgule flottante fonctionne "parfaitement", c'est-à-dire avec une précision exacte, lorsqu'on opère sur des valeurs intégrales comprises dans la plage du format. Cela signifie que si vous vous en tenez à double vous obtenez de très bons entiers d'un peu plus de 50 bits, ce qui vous donne environ +- 4 500 000 000 000 000, soit 4,5 quadrillions.

En fait, c'est ainsi que JavaScript fonctionne en interne, et c'est pourquoi JavaScript peut faire des choses comme + et - sur de très gros chiffres, mais ne peut << et >> sur ceux de 32 bits.

Strictement parlant, vous pouvez exactement comparer des sommes et des produits de nombres avec des représentations précises. Il s'agit de tous les nombres entiers, plus les fractions composées de 1 / 2 n termes. Ainsi, une boucle incrémentant par n + 0,25, n + 0,50, ou n + 0.75 conviendrait, mais pas l'une des 96 autres fractions décimales à deux chiffres.

Donc la réponse est : si l'égalité exacte peut en théorie avoir un sens dans des cas étroits, il vaut mieux l'éviter.

4 votes

Évidemment, c'est aussi parfait pour, par exemple, les valeurs qui peuvent être exprimées sous forme d'entiers dans la fourchette, divisés par une puissance de 2. La conclusion (légèrement facétieuse) : En d'autres termes, les flottants sont parfaits pour les valeurs qui peuvent être exprimées sous forme de flottants.

1 votes

Mais si on allait trop loin, n'obtiendrait-on pas quelque chose de bizarre comme 1 000 000 000 000 000 000 000 001 = 1 000 000 000 000 000 000 000 retournant à la réalité ?

1 votes

Et alors ? Si vous allez trop haut avec les int's, ils s'enroulent autour, ce qui est encore pire.

7voto

ulidtko Points 3834

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.

0 votes

Ne pourriez-vous pas simplement tester que d est inférieur à un petit nombre ?

0 votes

Oui, je pourrais, mais pourquoi ? Je peux de manière fiable s'arrêter à la précision maximale.

1 votes

Je ne dis pas que je préconise l'utilisation de la virgule flottante == comme jamais, mais c'est une réponse bien raisonnée, bien pensée et je lui ai donné un +1.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X