Aujourd'hui, je parcourais du code C++ (écrit par quelqu'un d'autre) et trouvai cette section:
double someValue = ...
if (someValue < std::numeric_limits<double>::epsilon() &&
someValue > -std::numeric_limits<double>::epsilon()) {
someValue = 0.0;
}
J'essaie de comprendre si cela a du sens.
La documentation de epsilon()
indique:
La fonction renvoie la différence entre 1 et la plus petite valeur supérieure à 1 pouvant être représentée [par un double].
Cela s'applique-t-il également à 0, c'est-à-dire que epsilon()
est la plus petite valeur supérieure à 0? Ou existe-t-il des nombres entre 0
Et 0 + epsilon
Pouvant être représentés par un double
?
Sinon, la comparaison n'est-elle pas équivalente à someValue == 0.0
?
En supposant le double IEEE 64 bits, il existe une mantisse 52 bits et un exposant 11 bits. Brisons-le en morceaux:
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 (1022 - 52 + 1)×2^52 = 4372995238176751616
nombres entre 0 et epsilon, ce qui représente 47% de tous les nombres représentables positifs ...
Le test n’est certainement pas le même que someValue == 0
. L'idée même des nombres à virgule flottante est qu'ils stockent un exposant et un significande. Ils représentent donc une valeur avec un certain nombre de valeurs de précision binaires significatives (53 dans le cas d’un double IEEE). Les valeurs représentables sont beaucoup plus compactées près de 0 que 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 exposant. Alors la prochaine valeur représentable 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 puisse stocker -4. Vous pouvez prendre ma parole pour cela qu'un double IEEE can stocke moins d'exposants que l'exposant de epsilon
.
A partir de ce code, vous ne pouvez pas dire s'il est logique ou non d'utiliser epsilon
spécifiquement comme lié, 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 immédiatement supérieur pouvant être représenté au-dessus de 1 et non la différence entre 0 et le nombre immédiatement supérieur pouvant être représenté au-dessus de 0 (si c'était le cas). le code ferait très peu): -
#include <limits>
int main ()
{
struct Doubles
{
double one;
double epsilon;
double half_epsilon;
} values;
values.one = 1.0;
values.epsilon = std::numeric_limits<double>::epsilon();
values.half_epsilon = values.epsilon / 2.0;
}
À l'aide d'un débogueur, arrêtez le programme à la fin du programme principal et examinez les résultats. Vous verrez que epsilon/2 est distinct de epsilon, zéro et un.
Donc, cette fonction prend des valeurs entre +/- epsilon et les met à zéro.
Une approximation de 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 réflexion montre clairement que plus la valeur de epsilon est petite, plus nous utilisons son nombre pour examiner sa valeur, car l’exposant peut s’adapter à la taille de ce nombre.
#include <stdio.h>
#include <assert.h>
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 à virgule flottante qui entrent dans un registre 16 bits. Il existe 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 binaire, multipliée par deux par la puissance de l'exposant.
Autour de 1, l'exposant est égal à zéro. Ainsi, le plus petit chiffre de la mantisse est une partie sur 1024.
Près de la moitié de l'exposant est moins un, de sorte que 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; à ce stade, la plus petite partie de la mantisse vaut une partie sur 32 m. Et à 16 exposant négatif, la valeur est d'environ une partie sur 32k, beaucoup plus proche de zéro que l'epsilon autour de celui que nous avons calculé ci-dessus!
Il s’agit d’un modèle jouet à virgule flottante qui ne reflète pas toutes les particularités d’un système réel, mais la possibilité de refléter des valeurs inférieures à epsilon est raisonnablement similaire à celle des valeurs réelles.
La différence entre X
et la valeur suivante de X
varie en fonction de X
.epsilon()
ne représente que la différence entre 1
et la valeur suivante de 1
.
La différence entre 0
Et la valeur suivante de 0
N'est pas epsilon()
.
À la place, vous pouvez utiliser std::nextafter
Pour comparer une valeur double à 0
Comme suit:
bool same(double a, double b)
{
return std::nextafter(a, std::numeric_limits<double>::lowest()) <= b
&& std::nextafter(a, std::numeric_limits<double>::max()) >= b;
}
double someValue = ...
if (same (someValue, 0.0)) {
someValue = 0.0;
}
Vous ne pouvez pas appliquer cela à 0, à cause des parties de la mantisse et de l'exposant. Grâce à exposant, vous pouvez stocker très peu de nombres, qui sont plus petits que epsilon, mais si vous essayez de faire quelque chose comme (1.0 - "très petit nombre"), vous obtenez 1.0. Epsilon est un indicateur non de valeur, mais de précision de valeur, qui est en mantisse. Il indique le nombre de chiffres décimaux consécutifs corrects que nous pouvons stocker.
Supposons donc que le système ne puisse pas distinguer les types 1.000000000000000000000 et 1.000000000000000000001. soit 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 la virgule flottante IEEE, entre la plus petite valeur positive non nulle et la plus petite valeur négative non nulle, il existe deux valeurs: zéro positif et zéro négatif. Tester si une valeur est comprise entre les plus petites valeurs non nulles revient à tester l'égalité avec zéro; l'assignation peut toutefois avoir un effet, car un zéro négatif deviendrait un zéro positif.
Il serait concevable qu'un format à virgule flottante ait trois valeurs entre les plus petites valeurs positives et négatives finies: l'infiniment petit positif, le zéro non signé et l'infiniment petit négatif. Je ne connais aucun des formats à virgule flottante qui fonctionnent de cette façon, mais un tel comportement serait parfaitement raisonnable et sans doute meilleur que celui de IEEE (peut-être pas assez pour mériter d'ajouter du matériel supplémentaire pour le prendre en charge, mais mathématiquement 1/(1/INF), 1/(- 1/INF) et 1/(1-1) doivent représenter trois cas distincts illustrant trois zéros différents). Je ne sais pas si une norme C obligerait les infinitésimaux signés, s’ils existent, à se comparer à zéro. Si ce n’est pas le cas, un code comme celui ci-dessus pourrait utilement garantir que, par exemple, diviser un nombre à plusieurs reprises par deux produirait finalement zéro plutôt que de rester bloqué sur "infinitésimal".
En outre, une bonne raison d'avoir une telle fonction consiste à supprimer les "dénormaux" (ces très petits nombres qui ne peuvent plus utiliser le premier signe implicite "1" et avoir une représentation spéciale FP). Pourquoi voudriez-vous le faire? Parce que certaines machines (en particulier des Pentium 4 plus anciens) deviennent vraiment très lentes lors du traitement des dénormaux. Si votre application n'a pas vraiment besoin de ces très petits nombres, rincer-les à zéro est une bonne solution.Les bons endroits à considérer constituent la dernière étape de tout filtre IIR ou de toute fonction de décroissance.
Voir aussi: Pourquoi changer 0.1f en 0 ralentit les performances de 10x?