¿Cómo debo hacer la comparación de punto flotante?

Actualmente estoy escribiendo un código en el que tengo algo parecido a:

double a = SomeCalculation1(); double b = SomeCalculation2(); if (a  b) DoSomething3(); 

Y luego en otros lugares puedo necesitar hacer igualdad:

 double a = SomeCalculation3(); double b = SomeCalculation4(); if (a == 0.0) DoSomethingUseful(1 / a); if (b == 0.0) return 0; // or something else here 

En resumen, tengo muchas matemáticas en coma flotante y necesito hacer varias comparaciones para las condiciones. No puedo convertirlo en matemáticas enteras porque tal cosa no tiene sentido en este contexto.

He leído antes que las comparaciones de coma flotante pueden no ser confiables, ya que puedes tener cosas como estas:

 double a = 1.0 / 3.0; double b = a + a + a; if ((3 * a) != b) Console.WriteLine("Oh no!"); 

En resumen, me gustaría saber: ¿cómo puedo comparar con fiabilidad los números de coma flotante (menor que, mayor que, la igualdad)?

El rango de números que uso es aproximadamente de 10E-14 a 10E6, así que necesito trabajar con números pequeños y grandes.

Lo he etiquetado como independiente del idioma porque estoy interesado en cómo puedo lograrlo sin importar el idioma que estoy usando.

Comparar para mayor / menor no es realmente un problema a menos que esté trabajando justo en el borde del límite de flotación / precisión doble.

Para una comparación de “iguales difusos”, este (el código Java, debería ser fácil de adaptar) es lo que se me ocurrió para The Floating-Point Guide después de mucho trabajo y teniendo en cuenta muchas críticas:

 public static boolean nearlyEqual(float a, float b, float epsilon) { final float absA = Math.abs(a); final float absB = Math.abs(b); final float diff = Math.abs(a - b); if (a == b) { // shortcut, handles infinities return true; } else if (a == 0 || b == 0 || diff < Float.MIN_NORMAL) { // a or b is zero or both are extremely close to it // relative error is less meaningful here return diff < (epsilon * Float.MIN_NORMAL); } else { // use relative error return diff / (absA + absB) < epsilon; } } 

Viene con un conjunto de prueba. Debe descartar inmediatamente cualquier solución que no lo haga, ya que está prácticamente garantizado que falla en algunos casos extremos, como tener un valor 0, dos valores muy pequeños opuestos a cero o infinitos.

Una alternativa (ver enlace arriba para más detalles) es convertir los patrones de bits de los flotadores a un número entero y aceptar todo dentro de una distancia entera fija.

En cualquier caso, probablemente no haya ninguna solución que sea perfecta para todas las aplicaciones. Lo ideal es que desarrolle / adapte su propio sistema de prueba que cubra sus casos de uso reales.

Tuve el problema de Comparar los números de punto flotante A < B y A > B Esto es lo que parece funcionar:

 if(A - B < Epsilon) && (fabs(AB) > Epsilon) { printf("A is less than B"); } if (A - B > Epsilon) && (fabs(AB) > Epsilon) { printf("A is greater than B"); } 

Las fábricas, valor absoluto, se ocupan de si son esencialmente iguales.

Tenemos que elegir un nivel de tolerancia para comparar los números de flotación. Por ejemplo,

 final float TOLERANCE = 0.00001; if (Math.abs(f1 - f2) < TOLERANCE) Console.WriteLine("Oh yes!"); 

Una nota. Tu ejemplo es bastante divertido.

 double a = 1.0 / 3.0; double b = a + a + a; if (a != b) Console.WriteLine("Oh no!"); 

Algunas matemáticas aquí

 a = 1/3 b = 1/3 + 1/3 + 1/3 = 1. 1/3 != 1 

Oh si..

Quieres decir

 if (b != 1) Console.WriteLine("Oh no!") 

TL; DR

  • Use la siguiente función en lugar de la solución actualmente aceptada para evitar algunos resultados no deseados en ciertos casos límites, mientras sea potencialmente más eficiente.
  • Conozca la imprecisión esperada que tiene en sus números y alimentarlos en consecuencia en la función de comparación.
 bool nearly_equal( float a, float b, float epsilon = 128 * FLT_EPSILON, float relth = FLT_MIN) // those defaults are arbitrary and could be removed { assert(std::numeric_limits::epsilon() <= epsilon); assert(epsilon < 1.f); if (a == b) return true; auto diff = std::abs(ab); auto norm = std::min((std::abs(a) + std::abs(b)), std::numeric_limits::max()); return diff < std::max(relth, epsilon * norm); } 

Gráficos, por favor?

Al comparar números de coma flotante, hay dos "modos".

El primero es el modo relativo , donde la diferencia entre y se considera con relación a su amplitud |x| + |y| |x| + |y| . Cuando se traza en 2D, da el siguiente perfil, donde verde significa igualdad de x y y . (Tomé un epsilon de 0.5 para fines de ilustración).

enter image description here

El modo relativo es lo que se usa para valores de puntos flotantes "normales" o "suficientemente grandes". (Más sobre eso más adelante).

El segundo es un modo absoluto , cuando simplemente comparamos su diferencia con un número fijo. Da el siguiente perfil (de nuevo con un epsilon de 0.5 y una relth de 1 para ilustración).

enter image description here

Este modo absoluto de comparación es lo que se usa para valores de punto flotante "pequeños".

Ahora la pregunta es, ¿cómo unimos esos dos patrones de respuesta?

En la respuesta de Michael Borgwardt, el cambio se basa en el valor de diff , que debe estar por debajo de relth ( Float.MIN_NORMAL en su respuesta). Esta zona de cambio se muestra sombreada en el gráfico a continuación.

enter image description here

Como relth * epsilon es más relth , los parches verdes no se unen, lo que a su vez da a la solución una mala propiedad: podemos encontrar trillizos de números tales que x < y_1 < y_2 y aún x == y2 pero x != y1 .

enter image description here

Toma este ejemplo sorprendente:

 x = 4.9303807e-32 y1 = 4.930381e-32 y2 = 4.9309825e-32 

Tenemos x < y1 < y2 , y de hecho y2 - x es más de 2000 veces más grande que y1 - x . Y sin embargo, con la solución actual,

 nearlyEqual(x, y1, 1e-4) == False nearlyEqual(x, y2, 1e-4) == True 

Por el contrario, en la solución propuesta anteriormente, la zona de conmutación se basa en el valor de |x| + |y| |x| + |y| , que está representado por el cuadrado rayado a continuación. Asegura que ambas zonas se conectan con gracia.

enter image description here

Además, el código anterior no tiene bifurcación, que podría ser más eficiente. Tenga en cuenta que las operaciones como max y abs , que a priori necesitan una bifurcación, a menudo tienen instrucciones de assembly dedicadas. Por esta razón, creo que este enfoque es superior a otra solución que sería corregir la nearlyEqual de Michael cambiando el cambio de diff < relth a diff < eps * relth , que luego produciría esencialmente el mismo patrón de respuesta.

¿Dónde cambiar entre comparación relativa y absoluta?

El cambio entre esos modos se realiza en torno a relth , que se toma como FLT_MIN en la respuesta aceptada. Esta elección significa que la representación de float32 es lo que limita la precisión de nuestros números de coma flotante.

Esto no siempre tiene sentido. Por ejemplo, si los números que compara son el resultado de una resta, tal vez algo en el rango de FLT_EPSILON tenga más sentido. Si son raíces cuadradas de números restados, la imprecisión numérica podría ser aún mayor.

Es bastante obvio cuando considera comparar un punto flotante con 0 . Aquí, cualquier comparación relativa fallará, porque |x - 0| / (|x| + 0) = 1 |x - 0| / (|x| + 0) = 1 . Entonces, la comparación necesita cambiar al modo absoluto cuando x está en el orden de la imprecisión de su cálculo, y raramente es tan bajo como FLT_MIN .

Esta es la razón para la introducción del parámetro relth anterior.

Además, al no multiplicar relth con epsilon , la interpretación de este parámetro es simple y corresponde al nivel de precisión numérica que esperamos en esos números.

Ruido matemático

(mantenido aquí principalmente para mi propio placer)

En términos más generales, supongo que un operador de comparación de coma flotante con buen comportamiento =~ debería tener algunas propiedades básicas.

Los siguientes son bastante obvios:

  • auto-igualdad: a =~ a
  • simetría: a =~ b implica b =~ a
  • invariancia por oposición: a =~ b implica -a =~ -b

(No tenemos a =~ b y b =~ c implica a =~ c , =~ no es una relación de equivalencia).

Agregaría las siguientes propiedades que son más específicas para las comparaciones de coma flotante

  • si a < b < c , entonces a =~ c implica a =~ b (los valores más cercanos también deberían ser iguales)
  • si a, b, m >= 0 entonces a =~ b implica a + m =~ b + m (los valores más grandes con la misma diferencia también deberían ser iguales)
  • si 0 <= λ < 1 entonces a =~ b implica λa =~ λb (quizás menos obvio para el argumento).

Esas propiedades ya otorgan fuertes restricciones a las posibles funciones de igualdad cercana. La función propuesta arriba los verifica. Quizás faltan una o varias propiedades obvias.

Cuando uno piensa en =~ como una familia de relación de igualdad =~[Ɛ,t] parametrizada por Ɛ y relth , también se podría agregar

  • si Ɛ1 < Ɛ2 entonces a =~[Ɛ1,t] b implica a =~[Ɛ2,t] b (igualdad para una tolerancia dada implica igualdad a una tolerancia más alta)
  • si t1 < t2 entonces a =~[Ɛ,t1] b implica a =~[Ɛ,t2] b (la igualdad para una imprecisión dada implica igualdad en una imprecisión más alta)

La solución propuesta también verifica esto.

Idea que tenía para la comparación de coma flotante en veloz

 infix operator ~= {} func ~= (a: Float, b: Float) -> Bool { return fabsf(a - b) < Float(FLT_EPSILON) } func ~= (a: CGFloat, b: CGFloat) -> Bool { return fabs(a - b) < CGFloat(FLT_EPSILON) } func ~= (a: Double, b: Double) -> Bool { return fabs(a - b) < Double(FLT_EPSILON) } 

El consejo estándar es usar un pequeño valor “épsilon” (elegido según su aplicación, probablemente), y considerar flotantes que estén dentro de épsilon uno del otro para que sean iguales. por ejemplo, algo así como

 #define EPSILON 0.00000001 if ((a - b) < EPSILON && (b - a) < EPSILON) { printf("a and b are about equal\n"); } 

Una respuesta más completa es complicada, porque el error de coma flotante es extremadamente sutil y confuso para razonar. Si realmente te importa la igualdad en algún sentido preciso, probablemente estés buscando una solución que no implique el punto flotante.

Adaptación a PHP de Michael Borgwardt y la respuesta de bosonix:

 class Comparison { const MIN_NORMAL = 1.17549435E-38; //from Java Specs // from http://floating-point-gui.de/errors/comparison/ public function nearlyEqual($a, $b, $epsilon = 0.000001) { $absA = abs($a); $absB = abs($b); $diff = abs($a - $b); if ($a == $b) { return true; } else { if ($a == 0 || $b == 0 || $diff < self::MIN_NORMAL) { return $diff < ($epsilon * self::MIN_NORMAL); } else { return $diff / ($absA + $absB) < $epsilon; } } } } 

La mejor manera de comparar los dobles para igualdad / desigualdad es tomar el valor absoluto de su diferencia y compararlo con un valor lo suficientemente pequeño (dependiendo de su contexto).

 double eps = 0.000000001; //for instance double a = someCalc1(); double b = someCalc2(); double diff = Math.abs(a - b); if (diff < eps) { //equal } 

Debería preguntarse por qué está comparando los números. Si conoce el propósito de la comparación, también debe conocer la precisión requerida de sus números. Eso es diferente en cada situación y en cada contexto de aplicación. Pero en casi todos los casos prácticos, se requiere una precisión absoluta . Es muy raro que se aplique una precisión relativa.

Para dar un ejemplo: si su objective es dibujar un gráfico en la pantalla, es probable que desee que los valores de punto flotante se igualen si se asignan al mismo píxel en la pantalla. Si el tamaño de su pantalla es 1000 píxeles, y sus números están en el rango 1e6, entonces probablemente querrá comparar 100 igual a 200.

Dada la precisión absoluta requerida, entonces el algoritmo se convierte en:

 public static ComparisonResult compare(float a, float b, float accuracy) { if (isnan(a) || isnan(b)) // if NaN needs to be supported return UNORDERED; if (a == b) // short-cut and takes care of infinities return EQUAL; if (abs(ab) < accuracy) // comparison wrt. the accuracy return EQUAL; if (a < b) // larger / smaller return SMALLER; else return LARGER; } 

Intenté escribir una función de igualdad teniendo en cuenta los comentarios anteriores. Esto es lo que se me ocurrió:

Editar: cambia de Math.Max ​​(a, b) a Math.Max ​​(Math.Abs ​​(a), Math.Abs ​​(b))

 static bool fpEqual(double a, double b) { double diff = Math.Abs(a - b); double epsilon = Math.Max(Math.Abs(a), Math.Abs(b)) * Double.Epsilon; return (diff < epsilon); } 

¿Pensamientos? Todavía necesito calcular un valor mayor que, y menos.

Debe tener en cuenta que el error de truncamiento es relativo. Dos números son casi iguales si su diferencia es aproximadamente tan grande como su ULP (Unidad en el último lugar).

Sin embargo, si realiza cálculos de coma flotante, su potencial de error aumenta con cada operación (especialmente cuidadosa con sustracciones), por lo que su tolerancia a los errores debe boost en consecuencia.

He estado tropezando con un problema similar recientemente, y he estado haciendo algunas pruebas.

En algunos casos, si dos flotantes tienen el mismo valor de cadena, serán comparativamente iguales en comparación con los flotantes (i..e, (float)$float1 === (float)$float2) , independientemente de cómo se derivaron. Sin embargo, en otros casos, incluso si dos flotantes tienen el mismo valor de cadena, a veces regresan como comparativamente diferentes cuando se comparan como flotantes si se derivaron de diferentes maneras.

Por favor mira el siguiente ejemplo:

 $float1 = 0.04 + 0.02; $float2 = 0.04 + 0.01 + 0.01; $float3 = 0.03 + 0.03; echo 'Values:'; var_dump($float1); echo '
'; var_dump($float2); echo '
'; var_dump($float3); echo '

'; echo 'Comparisons:'; var_dump($float1 - $float2); echo '
'; var_dump($float2 - $float3); echo '
'; var_dump($float1 - $float3); echo '
';

Ejecutar en PHP 5.3, aquí está el resultado:

Valores:

flotar (0.06)

flotar (0.06)

flotar (0.06)

Comparaciones

flotar (-6.93889390391E-18)

flotar (6.93889390391E-18)

flotar (0)

Como puede ver, $ float2 no es lo mismo que $ float1 y $ float3 cuando se compara como floats. La única diferencia entre ellos es cómo se derivaron. Se podría pensar que tiene sentido suponer que no importaría cómo se derivó un flotante, solo cuál es su valor final, pero del ejemplo anterior se puede ver que es una mala suposición.

La diferencia real es tan minúscula que en realidad no sería importante para hacer cálculos, pero SÍ importa al compararlos.

Si desea comparar flotadores como flotadores con confianza, esto es lo que sugiero antes de hacer una comparación:

 $float1 = 0.04 + 0.02; $float2 = 0.04 + 0.01 + 0.01; $float3 = 0.03 + 0.03; //Cast to string, then back to float $float1 = (float)(string)$float1; $float2 = (float)(string)$float2; $float3 = (float)(string)$float3; echo 'Values:'; var_dump($float1); echo '
'; var_dump($float2); echo '
'; var_dump($float3); echo '

'; echo 'Comparisons:'; var_dump($float1 - $float2); echo '
'; var_dump($float2 - $float3); echo '
'; var_dump($float1 - $float3); echo '
';

Valores:

flotar (0.06)

flotar (0.06)

flotar (0.06)

Comparaciones

flotar (0)

flotar (0)

flotar (0)

El problema que creo proviene de la forma en que las partes constituyentes de un flotador se redondean con diferente precisión para almacenarse en binario en una computadora. Si lanza a una cadena y luego vuelve a flotar, el redondeo se realiza en el valor final, de modo que si los valores finales tienen los mismos valores de cadena, sus valores de flotación también serán siempre los mismos.

Espero que ayude.

EDITAR:

Acabo de encontrar funciones matemáticas de BC (cálculo binario) en php.net. Estos parecen lograr lo mismo que arriba, sin embargo, devuelven valores de cadena, por lo que si quieres flotadores, asegúrate de volver a flotar después. Aquí está la documentación: http://php.net/manual/en/ref.bc.php

Vea abajo:

 echo '$float1 - $float2 = '; var_dump(bcsub($float1,$float2,2)); echo '
'; echo '$float2 - $float3 = '; var_dump(bcsub($float2,$float3,2)); echo '
'; echo '$float1 - $float3 = '; var_dump(bcsub($float1,$float3,2)); echo '
';

Devoluciones:

$ float1 – $ float2 = cadena (4) “0.00”

$ float2 – $ float3 = cadena (4) “0.00”

$ float1 – $ float3 = cadena (4) “0.00”