Est-il dangereux de comparer des valeurs à virgule flottante?


391

Je connais les UIKitutilisations en CGFloatraison du système de coordonnées indépendant de la résolution.

Mais chaque fois que je veux vérifier si par exemple frame.origin.xest 0il me rend malade:

if (theView.frame.origin.x == 0) {
    // do important operation
}

N'est pas CGFloatvulnérable aux faux positifs lorsque l'on compare avec ==, <=, >=, <, >? C'est une virgule flottante et ils ont des problèmes d'imprécision: 0.0000000000041par exemple.

La Objective-Cmanipulation est- elle interne lors de la comparaison ou peut-il arriver qu'un origin.xqui se lit comme zéro ne se compare pas à 0vrai?

Réponses:


466

Tout d'abord, les valeurs à virgule flottante ne sont pas "aléatoires" dans leur comportement. Une comparaison exacte peut et a du sens dans de nombreux usages du monde réel. Mais si vous souhaitez utiliser la virgule flottante, vous devez savoir comment cela fonctionne. Se tromper en supposant que les virgules flottantes fonctionnent comme des nombres réels vous obtiendrez un code qui se casse rapidement. Se tromper du côté de l'hypothèse que les résultats en virgule flottante sont associés à de grandes fuzz aléatoires (comme la plupart des réponses suggèrent ici) vous obtiendra un code qui semble fonctionner au premier abord mais finit par avoir des erreurs de grande ampleur et des cas de coin brisés.

Tout d'abord, si vous souhaitez programmer avec virgule flottante, vous devez lire ceci:

Ce que tout informaticien devrait savoir sur l'arithmétique à virgule flottante

Oui, lisez tout. Si c'est trop lourd, vous devez utiliser des entiers / point fixe pour vos calculs jusqu'à ce que vous ayez le temps de le lire. :-)

Maintenant, cela dit, les plus gros problèmes avec les comparaisons exactes en virgule flottante se résument à:

  1. Le fait que de nombreuses valeurs que vous pouvez écrire dans la source, ou lire avec scanfou strtod, n'existent pas en tant que valeurs à virgule flottante et sont converties en silence à l'approximation la plus proche. C'est ce dont parlait la réponse de demon9733.

  2. Le fait que de nombreux résultats soient arrondis en raison du manque de précision pour représenter le résultat réel. Un exemple simple où vous pouvez le voir est l'ajout x = 0x1fffffeet y = 1les flottants. Ici, xa 24 bits de précision dans la mantisse (ok) et yn'a que 1 bit, mais lorsque vous les ajoutez, leurs bits ne sont pas à des endroits qui se chevauchent, et le résultat aurait besoin de 25 bits de précision. Au lieu de cela, il est arrondi ( 0x2000000dans le mode d'arrondi par défaut).

  3. Le fait que de nombreux résultats soient arrondis en raison de la nécessité d'avoir infiniment de places pour la valeur correcte. Cela inclut à la fois des résultats rationnels comme 1/3 (que vous connaissez depuis la décimale où il prend infiniment de places) mais aussi 1/10 (qui prend également infiniment de places en binaire, car 5 n'est pas une puissance de 2), ainsi que des résultats irrationnels comme la racine carrée de tout ce qui n'est pas un carré parfait.

  4. Double arrondi. Sur certains systèmes (en particulier x86), les expressions à virgule flottante sont évaluées avec une précision supérieure à leurs types nominaux. Cela signifie que lorsque l'un des types d'arrondi ci-dessus se produit, vous obtiendrez deux étapes d'arrondi, d'abord un arrondi du résultat au type plus précis, puis un arrondi au type final. Par exemple, considérez ce qui se passe en décimal si vous arrondissez 1,49 à un entier (1), par rapport à ce qui se passe si vous l'arrondissez d'abord à une décimale (1,5) puis arrondissez ce résultat à un entier (2). C'est en fait l'un des domaines les plus désagréables à traiter en virgule flottante, car le comportement du compilateur (en particulier pour les compilateurs non conformes, comme GCC) est imprévisible.

  5. Fonctions transcendantes ( trig, exp, log, etc.) ne sont pas spécifiés pour avoir des résultats correctement arrondis; le résultat est juste spécifié pour être correct dans une unité à la dernière place de précision (généralement appelé 1ulp ).

Lorsque vous écrivez du code à virgule flottante, vous devez garder à l'esprit ce que vous faites avec les nombres qui pourraient rendre les résultats inexacts et effectuer des comparaisons en conséquence. Souvent, il sera judicieux de comparer avec un "epsilon", mais cet epsilon doit être basé sur l' ampleur des nombres que vous comparez , et non sur une constante absolue. (Dans les cas où un epsilon constant absolu fonctionnerait, cela indique fortement que le point fixe, et non le point flottant, est le bon outil pour le travail!)

Edit: En particulier, une vérification epsilon relative à la magnitude devrait ressembler à:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))

D'où FLT_EPSILONvient la constante float.h(remplacez-la par DBL_EPSILONpour doubles ou LDBL_EPSILONpour long doubles) et Kest une constante que vous choisissez de telle sorte que l'erreur cumulée de vos calculs soit définitivement limitée par des Kunités en dernier lieu (et si vous n'êtes pas sûr d'avoir obtenu l'erreur calcul lié à droite, faire Kquelques fois plus grand que ce que vos calculs disent qu'il devrait être).

Enfin, notez que si vous utilisez ceci, des précautions particulières peuvent être nécessaires près de zéro, car cela FLT_EPSILONn'a pas de sens pour les dénormals. Une solution rapide serait de le faire:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)

et également remplacer DBL_MINsi vous utilisez des doubles.


25
fabs(x+y)est problématique si xet y(peut) avoir un signe différent. Pourtant, une bonne réponse contre la vague de comparaisons fret-culte.
Daniel Fischer

27
Si xety ont un signe différent, ce n'est pas un problème. Le côté droit sera « trop petit », mais depuis xet yont signe différent, ils ne doivent pas être égaux de toute façon. (À moins qu'ils ne soient si petits qu'ils soient dénormaux, mais le deuxième cas le rattrape)
R .. GitHub STOP HELPING ICE

4
Je suis curieux de votre déclaration: "en particulier pour les compilateurs non conformes buggy comme GCC". Est-ce vraiment un buggy GCC et également non conforme?
Nicolás Ozimica

3
Étant donné que la question est étiquetée iOS, il convient de noter que les compilateurs d'Apple (à la fois les versions de clang et gcc d'Apple) ont toujours utilisé FLT_EVAL_METHOD = 0, et tentent d'être complètement stricts sur le fait de ne pas avoir une précision excessive. Si vous constatez des violations de cela, veuillez déposer des rapports de bogues.
Stephen Canon

17
"Tout d'abord, les valeurs à virgule flottante ne sont pas" aléatoires "dans leur comportement. Une comparaison exacte peut et a du sens dans de nombreuses utilisations réelles." - Juste deux phrases et déjà gagné un +1! C'est l'une des hypothèses les plus troublantes que les gens font lorsqu'ils travaillent avec des virgules flottantes.
Christian Rau

36

Étant donné que 0 est exactement représentable comme un nombre à virgule flottante IEEE754 (ou en utilisant toute autre implémentation de nombres fp avec laquelle j'ai déjà travaillé), la comparaison avec 0 est probablement sûre. Vous pourriez toutefois être mordu si votre programme calcule une valeur (telle quetheView.frame.origin.x ) que vous avez des raisons de croire qu'elle devrait être 0 mais que votre calcul ne peut garantir à 0.

Pour clarifier un peu, un calcul tel que:

areal = 0.0

va (à moins que votre langue ou votre système ne soit cassé) créer une valeur telle que (areal == 0.0) renvoie vrai mais un autre calcul tel que

areal = 1.386 - 2.1*(0.66)

Peut-être pas.

Si vous pouvez vous assurer que vos calculs produisent des valeurs qui sont 0 (et pas seulement qu'ils produisent des valeurs qui devraient être 0), alors vous pouvez continuer et comparer les valeurs fp à 0. Si vous ne pouvez pas vous assurer au degré requis , il vaut mieux s'en tenir à l'approche habituelle de «l'égalité tolérée».

Dans le pire des cas, la comparaison négligente des valeurs fp peut être extrêmement dangereuse: pensez à l'avionique, au guidage des armes, aux opérations de la centrale électrique, à la navigation dans les véhicules, à presque toutes les applications dans lesquelles le calcul rencontre le monde réel.

Pour Angry Birds, pas si dangereux.


11
En fait, 1.30 - 2*(0.65)est un exemple parfait d'une expression qui est évidemment évaluée à 0,0 si votre compilateur implémente IEEE 754, car les doubles représentés comme 0.65et 1.30ont les mêmes significations, et la multiplication par deux est évidemment exacte.
Pascal Cuoq

7
Je reçois toujours des représentants de celui-ci, j'ai donc changé le deuxième exemple d'extrait.
High Performance Mark

22

Je veux donner une réponse un peu différente des autres. Ils sont parfaits pour répondre à votre question comme indiqué, mais probablement pas pour ce que vous devez savoir ou quel est votre véritable problème.

La virgule flottante dans les graphiques est très bien! Mais il n'est presque pas nécessaire de comparer directement les flotteurs. Pourquoi auriez-vous besoin de faire ça? Les graphiques utilisent des flottants pour définir les intervalles. Et comparer si un flotteur est dans un intervalle également défini par des flotteurs est toujours bien défini et doit simplement être cohérent, pas exact ou précis! Tant qu'un pixel (qui est également un intervalle!) Peut être attribué, c'est tous les besoins graphiques.

Donc, si vous voulez tester si votre point est en dehors d'une plage de [0..largeur], c'est très bien. Assurez-vous simplement de définir l'inclusion de manière cohérente. Par exemple, définissez toujours inside is (x> = 0 && x <width). Il en va de même pour les tests d'intersection ou de hit.

Cependant, si vous abusez d'une coordonnée graphique comme une sorte d'indicateur, comme par exemple pour voir si une fenêtre est ancrée ou non, vous ne devriez pas le faire. Utilisez plutôt un indicateur booléen distinct de la couche de présentation graphique.


13

Comparé à zéro peut être une opération sûre, tant que le zéro n'est pas une valeur calculée (comme indiqué dans une réponse ci-dessus). La raison en est que zéro est un nombre parfaitement représentable en virgule flottante.

En parlant de valeurs parfaitement représentables, vous obtenez 24 bits de plage dans une notion de puissance de deux (simple précision). Donc, 1, 2, 4 sont parfaitement représentables, tout comme 0,5, 0,25 et 0,125. Tant que tous vos bits importants sont en 24 bits, vous êtes en or. 10.625 peut donc être représenté avec précision.

C'est très bien, mais va rapidement s'effondrer sous la pression. Deux scénarios viennent à l'esprit: 1) Lorsqu'un calcul est impliqué. Ne faites pas confiance à sqrt (3) * sqrt (3) == 3. Il n'en sera tout simplement pas ainsi. Et ce ne sera probablement pas dans un epsilon, comme le suggèrent certaines des autres réponses. 2) Lorsqu'une non-puissance de 2 (NPOT) est impliquée. Cela peut donc sembler étrange, mais 0,1 est une série infinie en binaire et donc tout calcul impliquant un nombre comme celui-ci sera imprécis dès le départ.

(Oh et la question d'origine mentionnait des comparaisons à zéro. N'oubliez pas que -0,0 est également une valeur à virgule flottante parfaitement valide.)


11

[La «bonne réponse» passe sous silence la sélection K. La sélection Kfinit par être aussi ponctuelle que la sélection, VISIBLE_SHIFTmais la sélection Kest moins évidente car, contrairement à VISIBLE_SHIFTelle, elle n'est basée sur aucune propriété d'affichage. Choisissez donc votre poison - sélectionnez Kou sélectionnez VISIBLE_SHIFT. Cette réponse préconise la sélection VISIBLE_SHIFT, puis démontre la difficulté de sélectionner K]

Précisément en raison d'erreurs rondes, vous ne devez pas utiliser la comparaison des valeurs «exactes» pour les opérations logiques. Dans votre cas spécifique d'une position sur un affichage visuel, il peut ne pas avoir d'importance si la position est de 0,0 ou 0,000000000003 - la différence est invisible à l'œil. Donc, votre logique devrait être quelque chose comme:

#define VISIBLE_SHIFT    0.0001        // for example
if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }

Cependant, à la fin, «invisible à l'œil» dépendra de vos propriétés d'affichage. Si vous pouvez limiter l'affichage (vous devriez pouvoir); puis choisissez VISIBLE_SHIFTd'être une fraction de cette limite supérieure.

Maintenant, la «bonne réponse» repose sur Knous allons donc explorer la sélection K. La «bonne réponse» ci-dessus dit:

K est une constante que vous choisissez de telle sorte que l'erreur cumulée de vos calculs soit définitivement limitée par K unités en dernier lieu (et si vous n'êtes pas sûr d'avoir correctement calculé le lien d'erreur, augmentez K quelques fois plus que ce que vos calculs dire que ça devrait être)

Nous avons donc besoin K. Si obtenir Kest plus difficile, moins intuitif que de sélectionner my, VISIBLE_SHIFTvous déciderez de ce qui vous convient . Pour trouver, Knous allons écrire un programme de test qui examine un tas de Kvaleurs afin que nous puissions voir comment il se comporte. Doit être évident comment choisir K, si la «bonne réponse» est utilisable. Non?

Nous allons utiliser, comme «bonne réponse» les détails:

if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)

Essayons simplement toutes les valeurs de K:

#include <math.h>
#include <float.h>
#include <stdio.h>

void main (void)
{
  double x = 1e-13;
  double y = 0.0;

  double K = 1e22;
  int i = 0;

  for (; i < 32; i++, K = K/10.0)
    {
      printf ("K:%40.16lf -> ", K);

      if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)
        printf ("YES\n");
      else
        printf ("NO\n");
    }
}
ebg@ebg$ gcc -o test test.c
ebg@ebg$ ./test
K:10000000000000000000000.0000000000000000 -> YES
K: 1000000000000000000000.0000000000000000 -> YES
K:  100000000000000000000.0000000000000000 -> YES
K:   10000000000000000000.0000000000000000 -> YES
K:    1000000000000000000.0000000000000000 -> YES
K:     100000000000000000.0000000000000000 -> YES
K:      10000000000000000.0000000000000000 -> YES
K:       1000000000000000.0000000000000000 -> NO
K:        100000000000000.0000000000000000 -> NO
K:         10000000000000.0000000000000000 -> NO
K:          1000000000000.0000000000000000 -> NO
K:           100000000000.0000000000000000 -> NO
K:            10000000000.0000000000000000 -> NO
K:             1000000000.0000000000000000 -> NO
K:              100000000.0000000000000000 -> NO
K:               10000000.0000000000000000 -> NO
K:                1000000.0000000000000000 -> NO
K:                 100000.0000000000000000 -> NO
K:                  10000.0000000000000000 -> NO
K:                   1000.0000000000000000 -> NO
K:                    100.0000000000000000 -> NO
K:                     10.0000000000000000 -> NO
K:                      1.0000000000000000 -> NO
K:                      0.1000000000000000 -> NO
K:                      0.0100000000000000 -> NO
K:                      0.0010000000000000 -> NO
K:                      0.0001000000000000 -> NO
K:                      0.0000100000000000 -> NO
K:                      0.0000010000000000 -> NO
K:                      0.0000001000000000 -> NO
K:                      0.0000000100000000 -> NO
K:                      0.0000000010000000 -> NO

Ah, donc K devrait être 1e16 ou plus si je veux que 1e-13 soit «zéro».

Donc, je dirais que vous avez deux options:

  1. Faites un calcul epsilon simple en utilisant votre jugement d'ingénierie pour la valeur de 'epsilon', comme je l'ai suggéré. Si vous faites des graphiques et que «zéro» est censé être un «changement visible», examinez vos ressources visuelles (images, etc.) et jugez ce que peut être epsilon.
  2. N'essayez pas de calculs en virgule flottante avant d'avoir lu la référence de la réponse non-cargo-culte (et obtenu votre doctorat dans le processus), puis utilisez votre jugement non intuitif pour sélectionner K.

10
Un aspect de l'indépendance de résolution est que vous ne pouvez pas dire avec certitude ce qu'est un "changement visible" au moment de la compilation. Ce qui est invisible sur un écran super HD pourrait très bien être évident sur un écran minuscule. Il faut au moins en faire une fonction de la taille de l'écran. Ou nommez-le quelque chose d'autre.
Romain

1
Mais au moins la sélection de «décalage visible» est basée sur des propriétés d'affichage (ou de trame) facilement compréhensibles - contrairement aux <réponses correctes> Kqui sont difficiles et non intuitives à sélectionner.
GoZoner

5

La bonne question: comment comparer les points dans Cocoa Touch?

La bonne réponse: CGPointEqualToPoint ().

Une question différente: deux valeurs calculées sont-elles identiques?

La réponse affichée ici: ils ne le sont pas.

Comment vérifier s'ils sont proches? Si vous souhaitez vérifier s'ils sont proches, n'utilisez pas CGPointEqualToPoint (). Mais ne vérifiez pas s'ils sont proches. Faites quelque chose qui a du sens dans le monde réel, comme vérifier pour voir si un point est au-delà d'une ligne ou si un point est à l'intérieur d'une sphère.


4

La dernière fois que j'ai vérifié la norme C, il n'était pas nécessaire que les opérations en virgule flottante sur les doubles (64 bits au total, mantisse 53 bits) soient plus précises que cette précision. Cependant, certains matériels peuvent effectuer les opérations dans des registres d'une plus grande précision, et l'exigence a été interprétée comme signifiant qu'il n'est pas nécessaire d'effacer les bits d'ordre inférieur (au-delà de la précision des nombres chargés dans les registres). Ainsi, vous pouvez obtenir des résultats inattendus de comparaisons comme celle-ci en fonction de ce qui reste dans les registres de celui qui y a dormi en dernier.

Cela dit, et malgré mes efforts pour l'effacer chaque fois que je le vois, la tenue où je travaille a beaucoup de code C qui est compilé en utilisant gcc et exécuté sur linux, et nous n'avons remarqué aucun de ces résultats inattendus depuis très longtemps. . Je ne sais pas si c'est parce que gcc efface les bits de poids faible pour nous, les registres 80 bits ne sont pas utilisés pour ces opérations sur les ordinateurs modernes, la norme a été modifiée ou quoi. J'aimerais savoir si quelqu'un peut citer un chapitre et un verset.


1

Vous pouvez utiliser un tel code pour comparer float avec zéro:

if ((int)(theView.frame.origin.x * 100) == 0) {
    // do important operation
}

Cela se comparera avec une précision de 0,1, suffisante pour CGFloat dans ce cas.


La diffusion vers intsans assurer theView.frame.origin.xest dans / près de cette plage de intconduits à un comportement indéfini (UB) - ou dans ce cas, 1 / 100e de la plage de int.
chux

Il n'y a absolument aucune raison de convertir en entier comme celui-ci. Comme l'a dit chux, il existe un potentiel d'UB à partir de valeurs hors limites; et sur certaines architectures, cela sera beaucoup plus lent que le simple calcul en virgule flottante. Enfin, multiplier par 100 comme cela se comparera avec une précision de 0,01 et non de 0,1.
Sneftel,

0
-(BOOL)isFloatEqual:(CGFloat)firstValue secondValue:(CGFloat)secondValue{

BOOL isEqual = NO;

NSNumber *firstValueNumber = [NSNumber numberWithDouble:firstValue];
NSNumber *secondValueNumber = [NSNumber numberWithDouble:secondValue];

isEqual = [firstValueNumber isEqualToNumber:secondValueNumber];

return isEqual;

}


0

J'utilise la fonction de comparaison suivante pour comparer un certain nombre de décimales:

bool compare(const double value1, const double value2, const int precision)
{
    int64_t magnitude = static_cast<int64_t>(std::pow(10, precision));
    int64_t intValue1 = static_cast<int64_t>(value1 * magnitude);
    int64_t intValue2 = static_cast<int64_t>(value2 * magnitude);
    return intValue1 == intValue2;
}

// Compare 9 decimal places:
if (compare(theView.frame.origin.x, 0, 9)) {
    // do important operation
}

-6

Je dirais que la bonne chose est de déclarer chaque nombre comme un objet, puis de définir trois choses dans cet objet: 1) un opérateur d'égalité. 2) une méthode setAcceptableDifference. 3) la valeur elle-même. L'opérateur d'égalité renvoie vrai si la différence absolue de deux valeurs est inférieure à la valeur définie comme acceptable.

Vous pouvez sous-classer l'objet en fonction du problème. Par exemple, des barres rondes en métal entre 1 et 2 pouces peuvent être considérées comme de diamètre égal si leurs diamètres diffèrent de moins de 0,0001 pouces. Vous appelez donc setAcceptableDifference avec le paramètre 0.0001, puis utilisez l'opérateur d'égalité en toute confiance.


1
Ce n'est pas une bonne réponse. Premièrement, toute la «chose objet» ne fait rien du tout pour résoudre votre problème. Et deuxièmement, votre mise en œuvre réelle de «l'égalité» n'est pas en fait la bonne.
Tom Swirly

3
Tom, peut-être que tu penserais à nouveau à la "chose objet". Avec des nombres réels, représentés avec une grande précision, l'égalité se produit rarement. Mais son idée de l'égalité peut être adaptée si elle vous convient. Ce serait plus agréable s'il y avait un opérateur "approximativement égal" remplaçable, mais il n'y en a pas.
John White
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.