46 votes

Égalité des points flottants

Il est de notoriété publique qu'il faut être prudent lorsqu'on compare des valeurs à virgule flottante. En général, 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 où l'on utilise == est parfaitement bien ?

Regardez ce simple extrait, quels sont les cas où le succès est garanti ?

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

Note : J'ai vérifié este y este mais ils ne couvrent pas (tous) mes cas.

Note2 : Il semble que je doive ajouter quelques informations supplémentaires, afin que les réponses puissent être utiles en pratique : J'aimerais savoir :

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

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

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

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


Je pense que cette question n'est pas un doublon du La virgule flottante == est-elle toujours acceptable ? . Cette question n'aborde aucun des cas que je pose. Même sujet, question différente. J'aimerais avoir des réponses spécifiques aux cas a)-d), pour lesquels je ne trouve pas de réponses dans la question dupliquée.

15voto

user2301274 Points 169

Cependant, je me demande s'il existe des cas où l'utilisation de == est parfaitement acceptable.

Bien sûr qu'il y en a. Une catégorie d'exemples est constituée par les utilisations qui n'impliquent aucun calcul, par exemple les setters qui ne doivent s'exécuter que lors des changements :

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 La virgule flottante == est-elle toujours acceptable ? y La virgule flottante == est-elle toujours acceptable ? .

6voto

William J Bagshaw Points 483

Les cas inventés peuvent "fonctionner". Les cas pratiques peuvent toujours é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 symboliquement, les résultats devraient être égaux, mais numériquement, ils sont différents. L'exemple ci-dessus pourrait, en théorie, échouer dans un tel cas. Certains compilateurs offrent une option permettant de produire des résultats plus cohérents, mais au détriment des performances. Je conseillerais de "toujours" éviter l'égalité des nombres à virgule flottante.

L'égalité des mesures physiques, ainsi que des flottants stockés numériquement, est souvent dénuée de sens. Donc, si vous comparez si les flottants sont égaux dans votre code, vous faites probablement quelque chose de mal. En général, vous voulez une valeur supérieure à, inférieure à ou comprise dans une tolérance. Souvent, le code peut être réécrit de manière à éviter ce type de problèmes.

5voto

cmaster Points 7460

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

Les cas c) et d) peuvent échouer parce que le calcul et la comparaison subséquente peuvent être effectués avec une précision supérieure à celle de l'algorithme de calcul. float . Les différents arrondis de double devrait être suffisant pour échouer le test.

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


Jargon juridique

En utilisant le projet de travail C++11 de la norme N3242, 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 de gauche n'est pas de type classe, l'expression est implicitement convertie (Clause 4) au type cv-unqualified de l'opérande de gauche.

La clause 4 fait référence aux conversions standard [conv], qui contiennent les éléments suivants sur les conversions à virgule flottante, [conv.double] 1 :

Une valeur de type virgule flottante peut être convertie en une valeur d'un autre type de virgule flottante. Si la source peut être représentée exactement dans le type de destination, le résultat de la conversion est cette représentation exacte. 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 l'implémentation de l'une ou l'autre de ces valeurs. Sinon, le comportement est indéfini.

(C'est moi qui souligne.)

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

Lorsque les gens pensent à "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 étendue supérieures à celles requises par le type. précision et une étendue plus grandes que celles requises par le type ; les types n'en sont pas modifiés.

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

Les opérateurs de cast et d'assignation doivent encore effectuer leurs conversions spécifiques, comme décrit en 5.4, 5.2.9 et 5.17.

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

Donc, quand je dis float a = some_double_expression; j'ai la garantie que le résultat de l'expression est effectivement arrondi pour être représentable par un float (n'invoquant UB que si la valeur est hors limites), et a fera référence à 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.

2voto

Cubic Points 5227

En supposant la sémantique IEEE 754, il y a certainement des cas où vous pouvez le faire. Les calculs conventionnels de nombres à virgule flottante sont exacts chaque fois qu'ils peuvent l'être, ce qui inclut par exemple (mais ne se limite pas à) toutes les opérations de base où les opérandes et les résultats sont des entiers.

Donc, si vous savez pertinemment que vous ne faites rien qui puisse donner lieu à quelque chose d'irreprésentable, tout va 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 se dégrade vraiment que si vous avez des calculs dont les résultats ne sont pas exactement représentables (ou qui impliquent des opérations qui ne sont pas exactes) et que vous changez l'ordre des opérations.

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

2voto

Steve Hollasch Points 56

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 calcul en virgule flottante (ou d'autres modes de calcul) sont modifiés au milieu de l'exécution de ce thread. J'ai vu cela en pratique, malheureusement.

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

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