Compare doble a cero usando epsilon

214

Hoy, estaba buscando 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 es representable [por un doble].

¿Esto se aplica también a 0, epsilon()es decir, el valor más pequeño es mayor que 0? ¿O hay números entre 0y 0 + epsilonque pueden ser representados por a double?

Si no, ¿no es la comparación equivalente a someValue == 0.0?

Sebastian Krysmanski
fuente
3
El épsilon alrededor de 1 probablemente sea mucho más alto que alrededor de 0, por lo que probablemente habrá valores entre 0 y 0 + epsilon_at_1. Supongo que el autor de esta sección quería usar algo pequeño, pero no quería usar una constante mágica, así que solo usó este valor esencialmente arbitrario.
enobayram
2
Comparar números de coma flotante es difícil, y hasta se recomienda el uso de epsilon o valor umbral. Consulte: cs.princeton.edu/introcs/91float y cygnus-software.com/papers/comparingfloats/comparingfloats.htm
Aditya Kumar Pandey
40
El primer enlace es 403.99999999
graham.reeds
66
OMI, en este caso, el uso de numeric_limits<>::epsilones engañoso e irrelevante. Lo que queremos es asumir 0 si el valor real difiere en no más de algunos ε de 0. Y ε debe elegirse en función de la especificación del problema, no en un valor dependiente de la máquina. Sospecho que el epsilon actual es inútil, ya que incluso unas pocas operaciones de FP podrían acumular un error mayor que eso.
Andrey Vihrov
1
+1. epsilon no es el más pequeño posible, pero puede cumplir el propósito dado en la mayoría de las tareas prácticas de ingeniería si sabe qué precisión necesita y qué está haciendo.
SChepurin

Respuestas:

192

Suponiendo que IEEE doble de 64 bits, hay una mantisa de 52 bits y un exponente de 11 bits. Vamos a romperlo en pedazos:

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 (1022 - 52 + 1)×2^52 = 4372995238176751616números entre 0 y épsilon, que es el 47% de todos los números representables positivos ...

Yakov Galka
fuente
27
Tan extraño que puedes decir "47% de los números positivos" :)
configurador
13
@configurator: No, no puedes decir eso (no existe una medida finita 'natural'). Pero puede decir "47% de los números representables positivos ".
Yakov Galka
1
@ybungalobill No puedo entenderlo. El exponente tiene 11 bits: 1 bit de signo y 10 bits de valor. ¿Por qué 2 ^ -1022 y no 2 ^ -1024 es el número positivo más pequeño?
Pavlo Dyban
3
@PavloDyban: simplemente porque los exponentes no tienen un bit de signo. Se codifican como compensaciones: si el exponente codificado es 0 <= e < 2048entonces, la mantisa se multiplica por 2 por la potencia de e - 1023. Por ejemplo, el exponente de 2^0se codifica como e=1023, 2^1como e=1024y 2^-1022como e=1. El valor de e=0está reservado para subnormales y el cero real.
Yakov Galka
2
@PavloDyban: también 2^-1022es el número normal más pequeño . El número más pequeño es en realidad 0.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^-1022 = 2^-1074. Esto es subnormal, lo que significa que la parte de mantisa es menor que 1, por lo que está codificada con el exponente e=0.
Yakov Galka
17

La prueba ciertamente no es lo mismo que someValue == 0. La idea de los números de coma 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 IEEE doble). Los valores representables están mucho más densos 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 1es 1.001 * 10^0, y epsilones 1.000 * 10^-3. Pero 1.000 * 10^-4también es representable, suponiendo que el exponente puede almacenar -4. Puede creer que un doble IEEE puede almacenar exponentes menores que el exponente de epsilon.

No puede distinguir solo con este código si tiene sentido o no usar epsilonespecíficamente como límite, debe mirar el contexto. Puede ser que epsilonsea ​​una estimación razonable del error en el cálculo que produjo someValue, y puede ser que no lo sea.

Steve Jessop
fuente
2
Buen punto, pero incluso si ese es el caso, una mejor práctica sería mantener el error vinculado en una variable razonablemente nombrada y usarlo en la comparación. Tal como está, no es diferente de una constante mágica.
enobayram
Quizás debería haber sido más claro en mi pregunta: no cuestioné si épsilon era un "umbral" lo suficientemente grande como para cubrir el error computacional, sino si esta comparación es igual someValue == 0.0o no.
Sebastian Krysmanski
13

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 se puede representar por encima de 1 y no la diferencia entre 0 y el siguiente número más alto que se puede representar por encima de 0 (si lo fuera, que el 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 epsilon / 2 es distinto de epsilon, cero y uno.

Entonces, esta función toma valores entre +/- epsilon y los hace cero.

Skizz
fuente
5

Con el siguiente programa se puede imprimir una aproximación de épsilon (diferencia más pequeña posible) alrededor de un número (1.0, 0.0, ...). Imprime el siguiente resultado:
epsilon for 0.0 is 4.940656e-324
epsilon for 1.0 is 2.220446e-16
un poco de reflexión deja en claro que el épsilon se hace más pequeño cuanto más pequeño es el número que usamos para observar su valor épsilon, porque el exponente puede ajustarse 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;
}
pbhd
fuente
2
¿Qué implementaciones has comprobado? Este definitivamente no es el caso para GCC 4.7.
Anton Golov
3

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 coma flotante es la mantisa, interpretada como un valor decimal binario, multiplicado por dos por la potencia del exponente.

Alrededor de 1 el exponente es igual a cero. Entonces, 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 alcanzar 16 negativos, en cuyo punto la parte más pequeña de la mantisa vale una parte en 32 m. Y en el exponente negativo 16, el valor es de alrededor de una parte en 32k, ¡mucho más cercano a cero que el épsilon alrededor de uno que calculamos arriba!

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.

Yakk - Adam Nevraumont
fuente
3

La diferencia entre Xy el siguiente valor de Xvaría según X.
epsilon()es solo la diferencia entre 1y el siguiente valor de 1.
La diferencia entre 0y el siguiente valor de 0no es epsilon().

En su lugar, puede usar std::nextafterpara comparar un valor doble con 0el 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;
}
Daniel Laügt
fuente
2

Creo que eso depende de la precisión de su computadora. Eche un vistazo a esta tabla : puede ver que si su épsilon está representado por el doble, pero su precisión es mayor, la comparación no es equivalente a

someValue == 0.0

Buena pregunta de todos modos!

Luca Davanzo
fuente
2

No puede aplicar esto a 0, debido a las partes de mantisa y exponente. Debido al exponente, puede almacenar números muy pequeños, que son más pequeños que épsilon, pero cuando intenta hacer algo como (1.0 - "número muy pequeño") obtendrá 1.0. Epsilon es un indicador no de valor, sino de precisión de valor, que está en mantisa. Muestra cuántos dígitos decimales consecuentes correctos de número podemos almacenar.

Arsenii Fomin
fuente
2

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 distintos de 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 pudiera tener tres valores entre los valores positivos y negativos finitos más pequeños: infinitesimal positivo, cero sin signo e 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 (quizás no sea lo suficientemente mejor como para que valga la pena 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 C exigiría que los infinitesimales firmados, si existen, tendrían que comparar igual a cero. Si no lo hacen, un código como el anterior podría garantizar que, por ejemplo,

Super gato
fuente
¿No es "1 / (1-1)" (según su ejemplo) infinito en lugar de cero?
Sebastian Krysmanski
Las cantidades (1-1), (1 / INF) y (-1 / INF) representan todas cero, pero dividir un número positivo por cada una de ellas debería en teoría arrojar tres resultados diferentes (la matemática IEEE considera que las dos primeras son idénticas )
supercat
1

Entonces, 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?

cababunga
fuente
Excepto por cero, no creo que haya valores entre -1e-20 y + 1e-20. Pero solo porque creo que esto no lo hace realidad.
Sebastian Krysmanski
@SebastianKrysmanski: no es cierto, hay muchos valores de punto flotante entre 0 y epsilon. Porque es punto flotante , no punto fijo.
Steve Jessop
El valor representable más pequeño que es distinto de cero está limitado por el número de bits asignados para representar el exponente. Entonces, si double tiene un exponente de 11 bits, el número más pequeño sería 1e-1023.
cababunga
0

Además, una buena razón para tener una función de este tipo es eliminar "denormals" (esos números muy pequeños que ya no pueden usar el "1" inicial implícito y tienen una representación FP especial). Por qué querrías hacer esto? Debido a que algunas máquinas (en particular, algunas Pentium 4s más antiguas) se vuelven muy, muy lentas al procesar denormals. Otros se vuelven un poco más lentos. Si su aplicación realmente no necesita estos números muy pequeños, vaciarlos a cero es una buena solución. Un buen lugar para considerar esto son los últimos pasos de cualquier filtro IIR o función de descomposición.

Ver también: ¿Por qué cambiar 0.1f a 0 ralentiza el rendimiento en 10x?

y http://en.wikipedia.org/wiki/Denormal_number

Dithermaster
fuente
1
Esto elimina muchos más números que solo los números desnormalizados. Cambia la constante de Planck o la masa de un electrón a cero, lo que te dará resultados muy, muy incorrectos si usas estos números.
gnasher729