web-dev-qa-db-fra.com

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

Je sais que UIKit utilise CGFloat en raison 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.x est 0, je me sens malade:

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

CGFloat n'est-il pas vulnérable aux faux positifs lors de la comparaison avec ==, <=, >=, <, >? C'est un point flottant et ils ont des problèmes de non précision: 0.0000000000041 par exemple.

Est-ce que Objective-C gère cela en interne lors de la comparaison ou peut-il arriver qu'un Origin.x qui se lit à zéro ne se compare pas à 0 comme vrai?

380
Proud Member

Tout d'abord, les valeurs en virgule flottante ne sont pas "aléatoires" dans leur comportement. Une comparaison exacte peut avoir un sens dans de nombreux usages réels. Mais si vous utilisez un point flottant, vous devez savoir comment cela fonctionne. En prenant pour acquis que la virgule flottante fonctionne comme si de vrais nombres obtiendraient un code qui se casse rapidement. Si vous vous trompez en supposant que les résultats en virgule flottante sont associés à un grand fuzz aléatoire (comme le suggèrent la plupart des réponses ci-dessous), vous obtiendrez un code qui semble fonctionner au début, mais qui finit par avoir des erreurs de grande magnitude et des cas de coupures.

Tout d’abord, si vous voulez programmer avec une virgule flottante, vous devriez lire ceci:

Ce que tous les informaticiens devraient savoir sur l’arithmétique en virgule flottante

Oui, lisez le tout. Si cela vous pose trop de problèmes, vous devez utiliser des entiers/points fixes pour vos calculs jusqu'à ce que vous ayez le temps de le lire. :-)

Cela dit, les principaux problèmes liés aux comparaisons exactes en virgule flottante se résument à:

  1. Le fait que vous puissiez écrire beaucoup dans le source ou lire avec scanf ou strtod, n’existent pas sous forme de valeurs à virgule flottante et converti silencieusement à 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 voir ceci est d'ajouter x = 0x1fffffe et y = 1 en tant que flottants. Ici, x a une précision de 24 bits dans la mantisse (ok) et y n'a qu'un bit, mais lorsque vous les ajoutez, leurs bits ne se superposent pas et le résultat nécessite 25 bits. de précision. Au lieu de cela, il est arrondi (à 0x2000000 dans le mode d'arrondi par défaut).

  3. Le fait que de nombreux résultats soient arrondis en raison du nombre infini d'emplacements nécessaires pour obtenir la valeur correcte. Cela inclut à la fois des résultats rationnels comme 1/3 (que vous connaissez bien de décimale où il faut une infinité de places) mais aussi 1/10 (qui prend également une infinité de places en binaire, puisque 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 (notamment x86), les expressions en virgule flottante sont évaluées avec une précision supérieure à celle de leurs types nominaux. Cela signifie que lorsque l'un des types d'arrondi ci-dessus se produit, vous obtenez deux étapes, la première en arrondissant le résultat au type de précision supérieure, puis en arrondissant le type final. Par exemple, considérons ce qui se passe en décimal si vous arrondissez 1,49 à un entier (1), par rapport à ce qui se passe si vous l’arrondez d’abord à une décimale (1,5), puis ce résultat à un entier (2). C’est en fait l’un des domaines les plus dangereux à traiter en virgule flottante, car le comportement du compilateur (en particulier pour les compilateurs non conformes tels que GCC) est imprévisible.

  5. Les fonctions transcendantales (trig, exp, log, etc.) ne sont pas spécifiées pour avoir des résultats correctement arrondis; le résultat est juste spécifié pour être correct dans une unité dans le dernier lieu 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 faire des comparaisons en conséquence. Souvent, il sera utile de comparer avec un "epsilon", mais epsilon devrait être basé sur la magnitude des nombres que vous comparez , et non sur une constante absolue. (Dans les cas où un epsilon constant fonctionnerait, cela indique clairement que le point fixe, et non la virgule flottante, est le bon outil pour le travail!)

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

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

FLT_EPSILON est la constante de float.h (remplacez-la par DBL_EPSILON pourdoubles ou LDBL_EPSILON pour long doubles) et K est un constante vous choisissez telle que l’erreur accumulée dans vos calculs soit définitivement délimitée par K unités à la dernière place (et si vous n'êtes pas sûr d’avoir correctement calculé le calcul de l’erreur, faites K plusieurs fois plus gros que ce que vos calculs disent, cela devrait être).

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

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

et remplacez également DBL_MIN si vous utilisez des doublons.

452
R..

Puisque 0 est exactement représentable sous forme de nombre à virgule flottante IEEE754 (ou à l'aide de toute autre implémentation de nombres f-p avec laquelle j'ai déjà travaillé), la comparaison avec 0 est probablement sans danger. Vous risquez cependant de vous faire mordre si votre programme calcule une valeur (telle que theView.frame.Origin.x) que vous avez des raisons de croire qu'elle devrait être 0 mais que votre calcul ne peut garantir d'être à 0.

Pour clarifier un peu, un calcul tel que:

areal = 0.0

will (sauf si votre langue ou votre système est cassé) crée une valeur telle que (areal == 0.0) renvoie la valeur true, 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), vous pouvez alors aller de l'avant et comparer les valeurs fp avec 0. Si vous ne pouvez pas vous assurer du degré requis , mieux vaut s'en tenir à l'approche habituelle de "l'égalité tolérée".

Dans le pire des cas, la comparaison négligente des valeurs de f-p peut être extrêmement dangereuse: pensez à l'avionique, au guidage des armes, au fonctionnement des centrales, à la navigation dans les véhicules, à presque toutes les applications dans lesquelles le calcul est conforme au monde réel.

Pour Angry Birds, pas si dangereux.

34

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, c'est bien! Mais il n’est presque pas nécessaire de comparer les flotteurs directement. Pourquoi auriez-vous besoin de faire ça? Graphics utilise des floats pour définir les intervalles. Et comparer si un float se situe dans un intervalle également défini par des floats est toujours bien défini et doit simplement être cohérent, non précis ou précis! Tant qu'un pixel (qui est aussi un intervalle!) Peut être attribué, il répond à tous les besoins graphiques.

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

Toutefois, si vous abusez d'une coordonnée graphique en tant qu'indicateur, par exemple pour savoir si une fenêtre est ancrée ou non, vous ne devriez pas procéder ainsi. Utilisez plutôt un indicateur booléen distinct de la couche de présentation graphique.

20
starmole

La comparaison à zéro peut est 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 une plage de 24 bits dans une notion de puissance de deux (simple précision). Donc, 1, 2, 4 sont parfaitement représentables, de même que 0,5, 0,25 et 0,125. Tant que tous vos bits importants sont en 24 bits, vous êtes en or. Donc 10.625 peut être représenté exactement.

C’est génial, mais s’effondrera rapidement sous la pression. Deux scénarios nous viennent à l’esprit: 1) lorsqu’un calcul est impliqué. Ne croyez pas que sqrt (3) * sqrt (3) == 3. Ce ne sera tout simplement pas ainsi. Et ce ne sera probablement pas dans les limites d'un epsilon, comme le suggèrent certaines des autres réponses. 2) Lorsqu'un non-pouvoir-de-2 (NPOT) est impliqué. Cela peut donc sembler étrange, mais 0.1 est une série infinie en binaire et, par conséquent, tout calcul impliquant un nombre comme celui-ci sera imprécis dès le départ.

(Oh et la question initiale mentionnait les comparaisons à zéro. N'oubliez pas que -0.0 est également une valeur à virgule flottante parfaitement valide.)

13
JHumphrey

[La "bonne réponse" passe au-dessus de la sélection de K. La sélection de K finit par être aussi ad hoc que celle de VISIBLE_SHIFT, mais la sélection de K est moins évidente car, contrairement à VISIBLE_SHIFT, elle n'est basée sur aucune propriété d'affichage. Choisissez votre poison - sélectionnez K ou sélectionnez VISIBLE_SHIFT. Cette réponse préconise de sélectionner VISIBLE_SHIFT puis montre la difficulté de sélectionner K]

Précisément à cause des erreurs d'arrondi, vous ne devez pas utiliser la comparaison des valeurs "exactes" pour les opérations logiques. Dans votre cas particulier de position sur un affichage visuel, peu importe que la position soit 0.0 ou 0.0000000003 - 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 pour les yeux" dépendra de vos propriétés d'affichage. Si vous pouvez utiliser la limite supérieure de l'affichage (vous devriez pouvoir le faire); puis choisissez VISIBLE_SHIFT comme une fraction de cette limite supérieure.

Maintenant, la "bonne réponse" repose sur K, explorons donc le choix de K. La "bonne réponse" ci-dessus dit:

K est une constante que vous choisissez de telle sorte que l’erreur accumulée dans vos calculs soit définitivement délimitée par K unités à la dernière place (et si vous n'êtes pas sûr d’avoir correctement calculé le calcul de l’erreur, donnez à K un peu plus grand que vos calculs. dire ce devrait être)

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

Nous allons utiliser comme détails de la "bonne réponse":

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

Essayons juste 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. Effectuez un calcul epsilon simple en utilisant votre jugement technique 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", alors examinez vos ressources visuelles (images, etc.) et évaluez ce que peut être epsilon.
  2. Ne tentez aucun calcul en virgule flottante avant d'avoir lu la référence de la réponse non liée au cargo (et obtenu votre doctorat par la suite), puis utilisez votre jugement non intuitif pour sélectionner K.
11
GoZoner

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

La réponse correcte: CGPointEqualToPoint ().

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

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

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

5
Michael

La dernière fois que j'ai vérifié la norme C, les opérations en virgule flottante sur les doubles (total 64 bits, mantisse 53 bits) n'étaient pas nécessaires pour être plus précises que cette précision. Cependant, certains matériels peuvent effectuer des opérations dans des registres de plus grande précision, et l'exigence a été interprétée comme ne nécessitant pas d'effacement des bits de poids faible (au-delà de la précision des nombres chargés dans les registres). Vous pouvez donc obtenir des résultats inattendus de ce type en fonction de ce qui reste dans les registres de celui qui a dormi la dernière fois.

Ceci dit, et malgré mes efforts pour le supprimer chaque fois que je le vois, la tenue sur laquelle je travaille a beaucoup de code C compilé avec 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.

4
Lucas Membrane

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

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

Cela permettra de comparer avec une précision de 0,1, ce qui est suffisant pour CGFloat dans ce cas.

1
Igor
-(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
Abbas Mulani