web-dev-qa-db-fra.com

Egalité à virgule flottante

Il est de notoriété publique qu'il faut faire attention lorsque l'on compare des valeurs à virgule flottante. Habituellement, au lieu d'utiliser ==, nous utilisons des tests d’égalité basés sur epsilon ou ULP.

Cependant, je me demande s’il existe des cas lorsqu’on utilise == va parfaitement bien?

Regardez ce simple extrait, quels sont les cas garantis pour réussir?

void fn(float a, float b) {
    float l1 = a/b;
    float l2 = a/b;

    if (l1==l1) { }        // case a)
    if (l1==l2) { }        // case b)
    if (l1==a/b) { }       // case c)
    if (l1==5.0f/3.0f) { } // case d)
}

int main() {
    fn(5.0f, 3.0f);
}

Remarque: j'ai vérifié this et this , mais ils ne couvrent pas (tous) mes cas.

Note 2: Il semble que je doive ajouter quelques informations supplémentaires. Les réponses peuvent donc être utiles dans la pratique: j'aimerais savoir:

  • ce que dit la norme C++
  • que se passe-t-il si une implémentation C++ suit IEEE-754

C’est la seule déclaration pertinente que j’ai trouvée dans le projet de norme actuel :

La représentation des valeurs des types à virgule flottante est définie par l'implémentation. [Remarque: Ce document n'impose aucune exigence quant à la précision des opérations en virgule flottante; voir aussi [support.limits]. - note de fin]

Donc, cela signifie-t-il que même le "cas a)" est défini comme implémentation? Je veux dire, l1==l1 est définitivement une opération à virgule flottante. Donc, si une implémentation est "inexacte", alors l1==l1 être faux?


Je pense que cette question n'est pas une copie de Est-ce que le virgule flottante == est toujours OK? . Cette question ne concerne aucun des cas que je pose. Même sujet, question différente. J'aimerais avoir des réponses spécifiques au cas a) -d), pour lequel je ne trouve pas de réponses dans la question dupliquée.

46
geza

Cependant, je me demande s’il existe des cas dans lesquels utiliser == convient parfaitement.

Bien sûr il y a. Une catégorie d’exemples concerne les utilisations qui ne nécessitent aucun calcul, par ex. les setters qui ne doivent s’exécuter que sur les modifications:

void setRange(float min, float max)
{
    if(min == m_fMin && max == m_fMax)
        return;

    m_fMin = min;
    m_fMax = max;

    // Do something with min and/or max
    emit rangeChanged(min, max);
}

Voir aussi Le nombre à virgule flottante == est-il toujours correct? et La virgule flottante == est-elle toujours valide?.

15
user2301274

Les cas résolus peuvent "fonctionner". Les cas pratiques peuvent encore échouer. Un autre problème est que l'optimisation entraîne souvent de petites variations dans la façon dont le calcul est effectué, de sorte que les résultats doivent être symboliquement égaux, mais numériquement différents. L'exemple ci-dessus pourrait, en théorie, échouer dans un tel cas. Certains compilateurs offrent la possibilité de produire des résultats plus cohérents tout en maintenant la performance. Je conseillerais "toujours" d'éviter l'égalité des nombres en virgule flottante.

L'égalité des mesures physiques, ainsi que des flotteurs stockés numériquement, n'a souvent aucun sens. Donc, si vous comparez si les flottants sont égaux dans votre code, vous faites probablement quelque chose de mal. Vous voulez généralement plus ou moins que ou dans une tolérance. Le code peut souvent être réécrit pour éviter ces types de problèmes.

6
William J Bagshaw

Seuls a) et b) ont la garantie de réussir toute implémentation saine (voir le jargon juridique ci-dessous pour plus de détails), car ils comparent deux valeurs dérivées de la même manière et arrondies à la précision float. Par conséquent, il est garanti que les deux valeurs comparées sont identiques au dernier bit.

Les cas c) et d) peuvent échouer car le calcul et la comparaison ultérieure peuvent être effectués avec une précision supérieure à float. L'arrondi différent de double devrait suffire à faire échouer le test.

Notez que les cas a) et b) peuvent encore échouer si des infinis ou des NAN sont impliqués, cependant.


Jargon juridique

En utilisant le brouillon de travail de la norme N3242 C++ 11, je trouve ce qui suit:

Dans le texte décrivant l'expression d'affectation, il est explicitement indiqué que la conversion de type a lieu, [expr.ass] 3:

Si l'opérande gauche n'est pas du type classe, l'expression est implicitement convertie (clause 4) en type cv-non qualifié de l'opérande gauche.

L'article 4 fait référence aux conversions standard [conv], qui contiennent les éléments suivants sur les conversions en virgule flottante, [conv.double] 1:

Une valeur de type virgule flottante peut être convertie en une valeur de type autre. Si la valeur source peut être représentée exactement dans le type de destination, le résultat de la conversion est cette représentation exacte. Si la valeur source est comprise entre deux valeurs de destination adjacentes, le résultat de la conversion est un choix défini par la mise en oeuvre de l'une ou l'autre de ces valeurs. Sinon, le comportement n'est pas défini.

(Souligné par moi.)

Nous avons donc la garantie que le résultat de la conversion est réellement défini, sauf si nous avons affaire à des valeurs hors de la plage représentable (comme float a = 1e300, qui est UB).

Quand les gens pensent que "la représentation interne en virgule flottante peut être plus précise que ce qui est visible dans le code", ils pensent à la phrase suivante de la norme, [expr] 11:

Les valeurs des opérandes flottants et les résultats des expressions flottantes peuvent être représentés avec une précision et une plage supérieures à celles requises par le type; les types ne sont pas modifiés de ce fait.

Notez que cela s'applique à opérandes et résultats, pas aux variables. Cela est souligné par la note de bas de page 60 ci-jointe:

Les opérateurs de distribution et d'assignation doivent toujours effectuer leurs conversions spécifiques, comme décrit aux sections 5.4, 5.2.9 et 5.17.

(Je suppose que c'est la note de bas de page que Maciej Piechotka entendait dans les commentaires - la numérotation semble avoir changé dans la version de la norme qu'il utilisait.)

Alors, quand je dis float a = some_double_expression;, J’ai la garantie que le résultat de l’expression est réellement arrondi pour pouvoir être représenté par un float (invoquant UB uniquement si la valeur est hors limites), et a se référer à cette valeur arrondie par la suite.

Une implémentation pourrait en effet spécifier que le résultat de l'arrondi est aléatoire et ainsi casser les cas a) et b). Les implémentations saines ne feront pas cela, cependant.

5
cmaster

En supposant la sémantique IEEE 754, il est tout à fait possible de faire cela. Les calculs de nombres à virgule flottante classiques sont exacts à chaque fois qu'ils peuvent l'être, ce qui inclut, par exemple, toutes les opérations de base dans lesquelles les opérandes et les résultats sont des entiers.

Donc, si vous savez pertinemment que vous ne faites rien qui résulterait en quelque chose d'irreprésentable, vous allez bien. Par exemple

float a = 1.0f;
float b = 1.0f;
float c = 2.0f;
assert(a + b == c); // you can safely expect this to succeed

La situation ne devient vraiment mauvaise que si vous avez des calculs avec des résultats qui ne sont pas exactement représentables (ou qui impliquent des opérations qui ne sont pas exactes) et vous modifiez l'ordre des opérations.

Notez que la norme C++ en elle-même ne garantit pas la sémantique IEEE 754, mais c'est ce à quoi vous pouvez vous attendre la plupart du temps.

2
Cubic

Le cas (a) échoue si a == b == 0.0. Dans ce cas, l'opération donne NaN, et par définition (IEEE, pas C) NaN ≠ NaN.

Les cas (b) et (c) peuvent échouer en calcul parallèle lorsque les modes de rondes en virgule flottante (ou d'autres modes de calcul) sont modifiés au milieu de l'exécution de ce fil. Vu celui-ci dans la pratique, malheureusement.

Le cas (d) peut être différent car le compilateur (sur une machine) peut choisir de plier de manière constante le calcul de 5.0f/3.0f et le remplacer par le résultat constant (de précision non spécifiée), tandis que a/b doit être calculé au moment de l'exécution sur la machine cible (ce qui peut être radicalement différent). En fait, les calculs intermédiaires peuvent être effectués avec une précision arbitraire. J'ai constaté des différences entre les anciennes architectures Intel lorsque les calculs intermédiaires étaient effectués en virgule flottante 80 bits, un format que le langage ne prenait même pas directement en charge.

2
Steve Hollasch

À mon humble avis, vous ne devriez pas compter sur le == opérateur car il a de nombreux cas de coin. Le plus gros problème est l'arrondi et la précision étendue. Dans le cas de x86, les opérations en virgule flottante peuvent être effectuées avec une précision plus grande que celle que vous pouvez stocker dans les variables (si vous utilisez des coprocesseurs, IIRC SSE les opérations utilisent la même précision que espace de rangement).

C’est généralement une bonne chose, mais cela pose des problèmes tels que: 1./2 != 1./2 parce qu’une valeur est une variable de forme et que la seconde provient d’un registre à virgule flottante. Dans les cas les plus simples, cela fonctionnera, mais si vous ajoutez d'autres opérations en virgule flottante, le compilateur pourrait décider de scinder certaines variables en pile, en modifiant leurs valeurs, modifiant ainsi le résultat de la comparaison.

Pour avoir une certitude à 100%, vous devez examiner Assembly et voir quelles opérations ont déjà été effectuées sur les deux valeurs. Même l'ordre peut changer le résultat dans des cas non triviaux.

Globalement, quel est l'intérêt d'utiliser ==? Vous devez utiliser des algorithmes stables. Cela signifie qu'ils fonctionnent même si les valeurs ne sont pas égales, mais ils donnent toujours les mêmes résultats. Le seul endroit où je sais où == pourrait être utile est de sérialiser/désérialiser où vous savez exactement quel résultat vous voulez et vous pouvez modifier la sérialisation pour archiver votre objectif.

0
Yankes