¿Es seguro verificar que los valores de punto flotante sean iguales a 0?

100

Sé que normalmente no puedes confiar en la igualdad entre valores de tipo doble o decimal, pero me pregunto si 0 es un caso especial.

Si bien puedo entender imprecisiones entre 0.00000000000001 y 0.00000000000002, 0 en sí mismo parece bastante difícil de estropear, ya que no es nada. Si eres impreciso en nada, ya no es nada.

Pero no sé mucho sobre este tema, así que no me corresponde a mí decirlo.

double x = 0.0;
return (x == 0.0) ? true : false;

¿Eso siempre volverá a ser verdad?

Gene Roberts
fuente
69
El operador ternario es redundante en ese código :)
Joel Coehoorn
5
LOL tienes razón. Go me
Gene Roberts
No lo haría porque no sabes cómo se puso x a cero. Si aún desea hacerlo, probablemente desee redondear o piso x para deshacerse del 1e-12 o algo así que podría estar etiquetado en el extremo.
Rex Logan

Respuestas:

115

Es seguro esperar que la comparación regrese truesi y solo si la variable doble tiene un valor de exactamente 0.0(que en su fragmento de código original es, por supuesto, el caso). Esto es consistente con la semántica del ==operador. a == bsignifica " aes igual a b".

No es seguro (porque no es correcto ) esperar que el resultado de algún cálculo sea cero en aritmética doble (o más generalmente, punto flotante) siempre que el resultado del mismo cálculo en Matemática pura sea cero. Esto se debe a que cuando los cálculos entran en juego, aparece un error de precisión de punto flotante, un concepto que no existe en la aritmética de números reales en matemáticas.

Daniel Daranas
fuente
51

Si necesita hacer muchas comparaciones de "igualdad", podría ser una buena idea escribir una pequeña función auxiliar o un método de extensión en .NET 3.5 para comparar:

public static bool AlmostEquals(this double double1, double double2, double precision)
{
    return (Math.Abs(double1 - double2) <= precision);
}

Esto podría usarse de la siguiente manera:

double d1 = 10.0 * .1;
bool equals = d1.AlmostEquals(0.0, 0.0000001);
Dirk Vollmar
fuente
4
Es posible que tenga un error de cancelación sustractiva al comparar double1 y double2, en caso de que estos números tengan valores muy cercanos entre sí. Quitaría Math.Abs ​​y comprobaría cada rama individualmente d1> = d2 - e y d1 <= d2 + e
Theodore Zographos
"Debido a que Epsilon define la expresión mínima de un valor positivo cuyo rango es cercano a cero, el margen de diferencia entre dos valores similares debe ser mayor que Epsilon. Por lo general, es muchas veces mayor que Epsilon. Por este motivo, le recomendamos que lo haga no utilice Epsilon al comparar valores dobles para la igualdad ". - msdn.microsoft.com/en-gb/library/ya2zha7s(v=vs.110).aspx
Rafael Costa
15

Para su muestra simple, esa prueba está bien. Pero ¿qué pasa con esto?

bool b = ( 10.0 * .1 - 1.0 == 0.0 );

Recuerde que .1 es un decimal periódico en binario y no se puede representar con exactitud. Luego compare eso con este código:

double d1 = 10.0 * .1; // make sure the compiler hasn't optimized the .1 issue away
bool b = ( d1 - 1.0 == 0.0 );

Dejaré que ejecute una prueba para ver los resultados reales: es más probable que lo recuerde de esa manera.

Joel Coehoorn
fuente
5
En realidad, esto devuelve verdadero por alguna razón (en LINQPad, al menos).
Alexey Romanov
¿Cuál es el "problema .1" del que hablas?
Teejay
14

Desde la entrada de MSDN para Double.Equals :

Precisión en las comparaciones

El método Equals debe usarse con precaución, porque dos valores aparentemente equivalentes pueden ser desiguales debido a la diferente precisión de los dos valores. El siguiente ejemplo informa que el valor Double .3333 y el Double devueltos al dividir 1 por 3 son desiguales.

...

En lugar de comparar la igualdad, una técnica recomendada implica definir un margen de diferencia aceptable entre dos valores (como el 0,01% de uno de los valores). Si el valor absoluto de la diferencia entre los dos valores es menor o igual a ese margen, es probable que la diferencia se deba a diferencias en la precisión y, por lo tanto, es probable que los valores sean iguales. El siguiente ejemplo usa esta técnica para comparar .33333 y 1/3, los dos valores Double que el ejemplo de código anterior encontró que no eran iguales.

Además, vea Double.Epsilon .

Stu Mackellar
fuente
1
También es posible comparar valores no del todo equivalentes como iguales. Uno esperaría eso si x.Equals(y), entonces (1/x).Equals(1/y), pero ese no es el caso si xes 0y yes 1/Double.NegativeInfinity. Esos valores se declaran iguales, aunque sus recíprocos no lo hacen.
supercat
@supercat: Son equivalentes. Y no tienen recíprocos. Podrías ejecutar tu prueba nuevamente con x = 0y y = 0, y aún así lo encontrarás 1/x != 1/y.
Ben Voigt
@BenVoigt: ¿Con xy ycomo tipo double? ¿Cómo se comparan los resultados para hacerlos reportar desiguales? Tenga en cuenta que 1 / 0.0 no es NaN.
supercat
@supercat: Ok, es una de las cosas que IEEE-754 se equivoca. (Primero, eso 1.0/0.0no es NaN como debería ser, ya que el límite no es único. En segundo lugar, los infinitos se comparan iguales entre sí, sin prestar atención a los grados de infinito)
Ben Voigt
@BenVoigt: Si el cero fue el resultado de multiplicar dos números muy pequeños, entonces dividir 1.0 en eso debería producir un valor que se compare mayor que cualquier número de los números pequeños que tengan el mismo signo, y menor que cualquier número si uno de los pequeños los números tenían signos opuestos. En mi humilde opinión, IEEE-754 sería mejor si tuviera un cero sin signo, pero infinitesimales positivos y negativos.
supercat
6

El problema surge cuando se comparan diferentes tipos de implementación de valor de punto flotante, por ejemplo, comparando float con double. Pero con el mismo tipo, no debería ser un problema.

float f = 0.1F;
bool b1 = (f == 0.1); //returns false
bool b2 = (f == 0.1F); //returns true

El problema es que el programador a veces olvida que el tipo implícito de conversión (doble a flotante) está sucediendo para la comparación y resulta en un error.

Yogee
fuente
3

Si el número se asignó directamente al flotante o al doble, es seguro realizar la prueba con cero o con cualquier número entero que se pueda representar en 53 bits para un doble o 24 bits para un flotante.

O para decirlo de otra manera, siempre puede asignar un valor entero a un doble y luego comparar el doble con el mismo número entero y tener la garantía de que será igual.

También puede comenzar asignando un número entero y hacer que las comparaciones simples sigan funcionando si se limita a sumar, restar o multiplicar por números enteros (asumiendo que el resultado es menos de 24 bits para un flotante y 53 bits para un doble). Por lo tanto, puede tratar los flotantes y los dobles como números enteros bajo ciertas condiciones controladas.

Kevin Gale
fuente
Estoy de acuerdo con su declaración en general (y la voté a favor), pero creo que realmente depende de si se usa o no la implementación de punto flotante IEEE 754. Y creo que todas las computadoras "modernas" usan IEEE 754, al menos para el almacenamiento de flotadores (hay reglas de redondeo extrañas que difieren).
Mark Lakata
2

No, no está bien. Los denominados valores desnormalizados (subnormales), cuando se comparan igual a 0.0, se compararían como falsos (distintos de cero), pero cuando se usan en una ecuación se normalizarían (se convertirían en 0.0). Por lo tanto, usar esto como un mecanismo para evitar una división por cero no es seguro. En su lugar, agregue 1.0 y compare con 1.0. Esto asegurará que todos los subnormales se traten como cero.


fuente
Los subnormales también se conocen como desnormales
Manuel
Los subnormales no se vuelven iguales a cero cuando se usan, aunque pueden producir o no el mismo resultado dependiendo de la operación exacta.
wnoise
-2

Pruebe esto y encontrará que == no es confiable para double / float.
double d = 0.1 + 0.2; bool b = d == 0.3;

Aquí está la respuesta de Quora.

rickyuu
fuente
-4

En realidad, creo que es mejor usar los siguientes códigos para comparar un valor doble con 0.0:

double x = 0.0;
return (Math.Abs(x) < double.Epsilon) ? true : false;

Lo mismo para flotador:

float x = 0.0f;
return (Math.Abs(x) < float.Epsilon) ? true : false;
David.Chu.ca
fuente
5
No. De los documentos de double.Epsilon: "Si crea un algoritmo personalizado que determina si dos números de punto flotante pueden considerarse iguales, debe usar un valor que sea mayor que la constante de Epsilon para establecer el margen de diferencia absoluto aceptable para que los dos valores se consideren iguales. (Normalmente, ese margen de diferencia es muchas veces mayor que el de Epsilon) "
Alastair Maw
1
@AlastairMaw esto se aplica a verificar la igualdad de dos dobles de cualquier tamaño. Para verificar la igualdad a cero, doble. Epsilon está bien.
jwg
4
No, es no . Es muy probable que el valor al que ha llegado a través de algún cálculo sea muchas veces épsilon de cero, pero aún así debería considerarse como cero. No logras mágicamente una gran cantidad de precisión adicional en tu resultado intermedio desde algún lugar, solo porque resulta ser cercano a cero.
Alastair Maw
4
Por ejemplo: (1.0 / 5.0 + 1.0 / 5.0 - 1.0 / 10.0 - 1.0 / 10.0 - 1.0 / 10.0 - 1.0 / 10.0) <double.Epsilon == false (y considerablemente en términos de magnitud: 2.78E-17 vs 4.94E -324)
Alastair Maw
Entonces, ¿cuál es la precisión recomendada, si es doble? ¿Epsilon no está bien? ¿Estarían bien 10 veces de épsilon? ¿100 veces?
liang