c++ - float - Compara doble a cero usando épsilon
long c++ (11)
Hoy, estaba revisando un código C ++ (escrito por otra persona) y encontré esta sección:
double someValue = ...
if (someValue < std::numeric_limits<double>::epsilon() &&
someValue > -std::numeric_limits<double>::epsilon()) {
someValue = 0.0;
}
Estoy tratando de averiguar si esto tiene sentido.
La documentación para epsilon()
dice:
La función devuelve la diferencia entre 1 y el valor más pequeño mayor que 1 que se puede representar [por un doble].
¿Esto se aplica también a 0, es decir, epsilon()
es el valor más pequeño mayor que 0? ¿O hay números entre 0
y 0 + epsilon
que pueden representarse por un double
?
Si no, ¿no es la comparación equivalente a someValue == 0.0
?
Además, una buena razón para tener dicha función es eliminar "denormales" (esos números muy pequeños que ya no pueden usar el encabezado implícito "1" y tienen una representación especial de PF). Por qué querrías hacer esto? Debido a que algunas máquinas (en particular, algunas Pentium 4 más antiguas) se vuelven realmente lentas al procesar denormales. Otros simplemente se vuelven un poco más lentos. Si su aplicación no necesita realmente estos números muy pequeños, tirarlos a cero es una buena solución. Los buenos lugares para considerar esto son los últimos pasos de cualquier filtro IIR o funciones de descomposición.
Ver también: ¿Por qué cambiar 0.1f a 0 ralentiza el rendimiento en 10x?
Así que digamos que el sistema no puede distinguir 1.000000000000000000000 y 1.000000000000000000001. eso es 1.0 y 1.0 + 1e-20. ¿Crees que todavía hay algunos valores que se pueden representar entre -1e-20 y + 1e-20?
Con el punto flotante IEEE, entre el valor positivo distinto de cero más pequeño y el valor negativo distinto de cero más pequeño, existen dos valores: cero positivo y cero negativo. Probar si un valor está entre los valores más pequeños que no sean cero es equivalente a probar la igualdad con cero; Sin embargo, la asignación puede tener un efecto, ya que cambiaría un cero negativo a un cero positivo.
Sería concebible que un formato de punto flotante tenga tres valores entre los valores finitos positivos y negativos más pequeños: infinitesimal positivo, cero sin signo y infinitesimal negativo. No estoy familiarizado con ningún formato de punto flotante que de hecho funcione de esa manera, pero tal comportamiento sería perfectamente razonable y posiblemente mejor que el de IEEE (tal vez no sea lo suficientemente mejor como para agregar hardware adicional para admitirlo, pero matemáticamente 1 / (1 / INF), 1 / (- 1 / INF), y 1 / (1-1) deben representar tres casos distintos que ilustran tres ceros diferentes). No sé si algún estándar de C exigiría que los infinitesimales firmados, si existieran, tuvieran que compararse igual a cero. Si no lo hacen, un código como el anterior podría asegurar que, por ejemplo, dividir un número repetidamente entre dos finalmente arrojaría un cero en lugar de quedarse atascado en "infinitesimal".
Hay números que existen entre 0 y épsilon porque épsilon es la diferencia entre 1 y el siguiente número más alto que puede representarse por encima de 1 y no la diferencia entre 0 y el siguiente número más alto que puede representarse por encima de 0 (si lo fuera, eso código haría muy poco): -
#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;
}
Usando un depurador, detenga el programa al final de main y mire los resultados y verá que épsilon / 2 es distinto de épsilon, cero y uno.
Así que esta función toma valores entre +/- epsilon y los hace cero.
La diferencia entre X
y el siguiente valor de X
varía según X
epsilon()
es solo la diferencia entre 1
y el siguiente valor de 1
.
La diferencia entre 0
y el siguiente valor de 0
no es epsilon()
.
En su lugar, puede usar std::nextafter
para comparar un valor doble con 0
como lo siguiente:
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;
}
La prueba ciertamente no es lo mismo que someValue == 0
. La idea general de los números de punto flotante es que almacenan un exponente y un significado. Por lo tanto, representan un valor con un cierto número de cifras binarias significativas de precisión (53 en el caso de un doble IEEE). Los valores representables son mucho más densamente empaquetados cerca de 0 que cerca de 1.
Para usar un sistema decimal más familiar, suponga que almacena un valor decimal "a 4 cifras significativas" con exponente. Entonces el siguiente valor representable mayor que 1
es 1.001 * 10^0
, y epsilon
es 1.000 * 10^-3
. Pero 1.000 * 10^-4
también es representable, asumiendo que el exponente puede almacenar -4. Puede confiar en que un doble IEEE puede almacenar exponentes menos que el exponente de epsilon
.
No se puede decir solo con este código si tiene sentido o no usar epsilon
específicamente como límite, debe mirar el contexto. Puede ser que epsilon
sea una estimación razonable del error en el cálculo que produjo someValue
, y puede ser que no lo sea.
No puedes aplicar esto a 0, debido a las partes de la mantisa y el exponente. Debido al exponente, puedes almacenar muy pocos números, que son más pequeños que épsilon, pero cuando intentas hacer algo como (1.0 - "número muy pequeño") obtendrás 1.0. Epsilon es un indicador no de valor, sino de precisión de valor, que está en la mantisa. Muestra cuántos dígitos decimales consecutivos correctos de número podemos almacenar.
Supongamos que estamos trabajando con números de punto flotante de juguete que caben en un registro de 16 bits. Hay un bit de signo, un exponente de 5 bits y una mantisa de 10 bits.
El valor de este número de punto flotante es la mantisa, interpretada como un valor decimal binario, multiplicado por dos a la potencia del exponente.
Alrededor de 1 el exponente es igual a cero. Así que el dígito más pequeño de la mantisa es una parte en 1024.
Cerca de la mitad del exponente es menos uno, por lo que la parte más pequeña de la mantisa es la mitad de grande. Con un exponente de cinco bits, puede llegar a 16 negativo, momento en el que la parte más pequeña de la mantisa vale una parte en 32 m. ¡Y con un exponente negativo de 16, el valor está alrededor de una parte en 32k, mucho más cerca de cero que el épsilon alrededor de uno que calculamos anteriormente!
Ahora, este es un modelo de punto flotante de juguete que no refleja todas las peculiaridades de un sistema de punto flotante real, pero la capacidad de reflejar valores más pequeños que épsilon es razonablemente similar con los valores de punto flotante real.
Suponiendo que IEEE doble de 64 bits, hay una mantisa de 52 bits y un exponente de 11 bits. Mira los siguientes números:
1.0000 00000000 00000000 00000000 00000000 00000000 00000000 × 2^0 = 1
El número representable más pequeño mayor que 1:
1.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^0 = 1 + 2^-52
Por lo tanto:
epsilon = (1 + 2^-52) - 1 = 2^-52
¿Hay algún número entre 0 y épsilon? Mucho ... Por ejemplo, el número mínimo positivo representable (normal) es:
1.0000 00000000 00000000 00000000 00000000 00000000 00000000 × 2^-1022 = 2^-1022
De hecho, hay alrededor de (1022 - 52 + 1)×2^52 = 4372995238176751616
números entre 0 y épsilon, que es aproximadamente el 47% de todos los números representables positivos ...
Una aproximación de épsilon (la menor diferencia posible) alrededor de un número (1.0, 0.0, ...) se puede imprimir con el siguiente programa. Imprime la siguiente salida:
epsilon for 0.0 is 4.940656e-324
epsilon for 1.0 is 2.220446e-16
Un poco de reflexión lo deja claro, que el épsilon se hace más pequeño cuanto más pequeño es el número que usamos para ver su valor de épsilon, porque el exponente se puede ajustar al tamaño de ese número.
#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;
}