Comparer double à zéro en utilisant epsilon

Aujourd’hui, j’ai parcouru du code C ++ (écrit par quelqu’un d’autre) et j’ai trouvé cette section:

double someValue = ... if (someValue < std::numeric_limits::epsilon() && someValue > -std::numeric_limits::epsilon()) { someValue = 0.0; } 

J’essaie de savoir si cela a du sens.

La documentation d’ epsilon() indique:

La fonction renvoie la différence entre 1 et la plus petite valeur supérieure à 1 qui est représentable [par un double].

Cela s’applique-t-il également à 0, c.-à-d. epsilon() est la plus petite valeur supérieure à 0? Ou y a-t-il des nombres entre 0 et 0 + epsilon qui peuvent être représentés par un double ?

Si non, alors la comparaison n’est-elle pas équivalente à someValue == 0.0 ?

En supposant un double IEEE 64 bits, il existe une mantisse de 52 bits et un exposant de 11 bits. Regardez les numéros suivants:

 1.0000 00000000 00000000 00000000 00000000 00000000 00000000 × 2^0 = 1 

Le plus petit nombre représentable supérieur à 1:

 1.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^0 = 1 + 2^-52 

Donc:

 epsilon = (1 + 2^-52) - 1 = 2^-52 

Y a-t-il des nombres entre 0 et epsilon? Beaucoup … Par exemple, le nombre minimum représentable positif (normal) est:

 1.0000 00000000 00000000 00000000 00000000 00000000 00000000 × 2^-1022 = 2^-1022 

En fait, il y a environ (1022 - 52 + 1)×2^52 = 4372995238176751616 nombres entre 0 et epsilon, ce qui représente environ 47% de tous les nombres représentables positifs …

Le test est certainement différent de someValue == 0 . L’idée générale des nombres à virgule flottante est qu’ils stockent un exposant et un significande. Ils représentent donc une valeur avec un certain nombre de chiffres significatifs de précision binarys (53 dans le cas d’un double IEEE). Les valeurs représentables sont beaucoup plus compactes près de 0 qu’elles ne sont près de 1.

Pour utiliser un système décimal plus familier, supposons que vous stockiez une valeur décimale «à 4 chiffres significatifs» avec un exposant. Ensuite, la valeur représentable suivante supérieure à 1 est 1.001 * 10^0 et epsilon est 1.000 * 10^-3 . Mais 1.000 * 10^-4 est également représentable, en supposant que l’exposant peut stocker -4. Vous pouvez prendre ma parole pour dire qu’un double IEEE peut stocker des exposants moins que l’exposant d’ epsilon .

À partir de ce code, vous ne pouvez pas savoir s’il est logique ou non d’utiliser spécifiquement epsilon , vous devez examiner le contexte. Il se peut que epsilon soit une estimation raisonnable de l’erreur dans le calcul qui a produit someValue , et il se peut que ce ne soit pas le cas.

Il existe des nombres entre 0 et epsilon car epsilon est la différence entre 1 et le nombre le plus élevé suivant pouvant être représenté au-dessus de 1 et non la différence entre 0 et le nombre le plus élevé suivant pouvant être représenté au-dessus de 0 le code ferait très peu): –

 #include  int main () { struct Doubles { double one; double epsilon; double half_epsilon; } values; values.one = 1.0; values.epsilon = std::numeric_limits::epsilon(); values.half_epsilon = values.epsilon / 2.0; } 

En utilisant un débogueur, arrêtez le programme à la fin de main et examinez les résultats et vous verrez qu’epsilon / 2 se distingue de epsilon, zéro et un.

Cette fonction prend donc des valeurs entre +/- epsilon et les rend nulles.

Une approximation d’epsilon (la plus petite différence possible) autour d’un nombre (1.0, 0.0, …) peut être imprimée avec le programme suivant. Il imprime la sortie suivante:
epsilon for 0.0 is 4.940656e-324
epsilon for 1.0 is 2.220446e-16
Un peu de reflection indique clairement que plus epsilon devient petit, plus le nombre que nous utilisons pour examiner sa valeur epsilon est faible, car l’exposant peut s’ajuster à la taille de ce nombre.

 #include  #include  double getEps (double m) { double approx=1.0; double lastApprox=0.0; while (m+approx!=m) { lastApprox=approx; approx/=2.0; } assert (lastApprox!=0); return lastApprox; } int main () { printf ("epsilon for 0.0 is %e\n", getEps (0.0)); printf ("epsilon for 1.0 is %e\n", getEps (1.0)); return 0; } 

Supposons que nous travaillions avec des nombres en virgule flottante qui correspondent à un registre de 16 bits. Il y a un bit de signe, un exposant de 5 bits et une mantisse de 10 bits.

La valeur de ce nombre à virgule flottante est la mantisse, interprétée comme une valeur décimale binary, multipliée par deux à la puissance de l’exposant.

Vers 1, l’exposant est égal à zéro. Donc, le plus petit chiffre de la mantisse est une partie en 1024.

Près de la moitié de l’exposant est moins un, donc la plus petite partie de la mantisse est deux fois moins grande. Avec un exposant de cinq bits, il peut atteindre une valeur négative de 16, point auquel la plus petite partie de la mantisse vaut une partie sur 32 m. Et à 16 exposants négatifs, la valeur est d’environ une partie en 32k, beaucoup plus proche de zéro que celle que nous avons calculée ci-dessus!

Maintenant, il s’agit d’un modèle en virgule flottante qui ne reflète pas tous les caprices d’un système à virgule flottante réelle, mais la capacité à refléter des valeurs inférieures à epsilon est raisonnablement similaire aux valeurs réelles en virgule flottante.

Je pense que cela dépend de la précision de votre ordinateur. Jetez un oeil sur cette table : vous pouvez voir que si votre epsilon est représenté par un double, mais que votre précision est supérieure, la comparaison n’est pas équivalente à

 someValue == 0.0 

Bonne question quand même!

Vous ne pouvez pas appliquer ceci à 0, à cause de la mantisse et des parties exposantes. En raison de l’exposant, vous pouvez stocker très peu de nombres, qui sont plus petits que epsilon, mais lorsque vous essayez de faire quelque chose comme (1.0 – “très petit nombre”), vous obtiendrez 1.0. Epsilon est un indicateur non pas de valeur, mais de précision de la valeur, qui est en mantisse. Il montre combien de chiffres décimaux consécutifs du nombre que nous pouvons stocker.

La différence entre X et la prochaine valeur de X varie selon X
epsilon() n’est que la différence entre 1 et la prochaine valeur de 1 .
La différence entre 0 et la prochaine valeur de 0 n’est pas epsilon() .

Au lieu de cela, vous pouvez utiliser std::nextafter pour comparer une valeur double avec 0 comme suit:

 bool same(double a, double b) { return std::nextafter(a, std::numeric_limits::lowest()) <= b && std::nextafter(a, std::numeric_limits::max()) >= b; } double someValue = ... if (same (someValue, 0.0)) { someValue = 0.0; } 

Alors, disons que le système ne peut pas distinguer 1.000000000000000000000 et 1.000000000000000000001. c’est-à-dire 1,0 et 1,0 + 1e-20. Pensez-vous qu’il existe encore des valeurs pouvant être représentées entre -1e-20 et + 1e-20?

Avec IEEE à virgule flottante, entre la plus petite valeur positive non nulle et la plus petite valeur négative non nulle, il existe deux valeurs: le zéro positif et le zéro négatif. Tester si une valeur est entre les plus petites valeurs non nulles équivaut à tester l’égalité avec zéro; l’affectation peut cependant avoir un effet, puisqu’elle modifierait un zéro négatif à un zéro positif.

Il serait concevable qu’un format à virgule flottante puisse avoir trois valeurs entre les plus petites valeurs positives et négatives finies: infinitésimal positif, zéro non signé et infinitésimal négatif. Je ne suis pas familier avec les formats en virgule flottante qui fonctionnent de cette manière, mais un tel comportement serait parfaitement raisonnable et sans doute meilleur que celui de l’IEEE (peut-être pas assez pour append du matériel supplémentaire pour le supporter, mais mathématiquement 1) / (1 / INF), 1 / (- 1 / INF) et 1 / (1-1) devraient représenter trois cas distincts illustrant trois zéros différents). Je ne sais pas si un standard C exigerait que les infinitésimaux signés, s’ils existent, doivent être égaux à zéro. S’ils ne le font pas, le code comme ci-dessus pourrait utilement garantir que, par exemple, diviser un nombre de façon répétée par deux produirait finalement zéro plutôt que d’être bloqué sur “infinitésimal”.

Aussi, une bonne raison d’avoir une telle fonction est de supprimer les “dénormaux” (ces très petits nombres qui ne peuvent plus utiliser le premier implicite “1” et qui ont une représentation FP spécifique). Pourquoi voudriez-vous faire cela? Parce que certaines machines (en particulier certains anciens Pentium 4) deviennent vraiment très lentes lors du traitement des dénormals. D’autres deviennent un peu plus lents. Si votre application n’a pas vraiment besoin de ces très petits nombres, les vider à zéro est une bonne solution. Les bons endroits à considérer sont les dernières étapes de tout filtre ou fonction de décroissance de l’IIF.

Voir aussi: Pourquoi le changement de 0.1f à 0 ralentit-il les performances de 10x?

et http://en.wikipedia.org/wiki/Normal_number