¿Cómo debo hacer una comparación de punto flotante?
Actualmente estoy escribiendo un código donde tengo algo como:
double a = SomeCalculation1();
double b = SomeCalculation2();
if (a < b)
DoSomething2();
else if (a > b)
DoSomething3();
Y luego, en otros lugares, es posible que necesite 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 muchos cálculos de punto flotante y necesito hacer varias comparaciones de condiciones. No puedo convertirlo a matemáticas de números enteros porque tal cosa no tiene sentido en este contexto.
He leído antes que las comparaciones de punto flotante pueden no ser confiables, ya que pueden suceder cosas como esta:
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 de manera confiable números de coma flotante (menor que, mayor que, igualdad)?
El rango de números que estoy usando es aproximadamente de 10E-14 a 10E6, por lo que necesito trabajar tanto con números pequeños como con números grandes.
He etiquetado esto como independiente del idioma porque estoy interesado en cómo puedo lograrlo sin importar el idioma que esté usando.
TL;DR
- Utilice la siguiente función en lugar de la solución actualmente aceptada para evitar algunos resultados no deseados en ciertos casos límite, siendo potencialmente más eficiente.
- Conozca la imprecisión esperada que tiene en sus números e introdúzcalos en consecuencia en la función de comparación.
bool nearly_equal(
float a, float b,
float epsilon = 128 * FLT_EPSILON, float abs_th = FLT_MIN)
// those defaults are arbitrary and could be removed
{
assert(std::numeric_limits<float>::epsilon() <= epsilon);
assert(epsilon < 1.f);
if (a == b) return true;
auto diff = std::abs(a-b);
auto norm = std::min((std::abs(a) + std::abs(b)), std::numeric_limits<float>::max());
// or even faster: std::min(std::abs(a + b), std::numeric_limits<float>::max());
// keeping this commented out until I update figures below
return diff < std::max(abs_th, epsilon * norm);
}
¿Gráficos, por favor?
Al comparar números de coma flotante, existen dos "modos".
El primero es el modo relativo , donde la diferencia entre x
y y
se considera relativa a su amplitud |x| + |y|
. Cuando se traza en 2D, se obtiene el siguiente perfil, donde el verde significa igualdad de x
y y
. (Tomé un epsilon
valor de 0,5 con fines ilustrativos).
El modo relativo es el que se utiliza para valores de coma flotante "normales" o "suficientemente grandes". (Más sobre esto más adelante).
El segundo es un modo absoluto , cuando simplemente comparamos su diferencia con un número fijo. Da el siguiente perfil (nuevamente con un epsilon
valor de 0,5 y un abs_th
valor de 1 a modo ilustrativo).
Este modo absoluto de comparación es el que se utiliza 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 debería estar por debajo abs_th
( Float.MIN_NORMAL
en su respuesta). Esta zona de cambio se muestra sombreada en el siguiente gráfico.
Como abs_th * epsilon
es más pequeño que abs_th
, los parches verdes no se pegan, lo que a su vez le da a la solución una mala propiedad: podemos encontrar tripletas de números tales que x < y_1 < y_2
y aún x == y2
pero x != y1
.
Tomemos este sorprendente ejemplo:
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 cambio se basa en el valor de |x| + |y|
, que está representado por el cuadrado sombreado a continuación. Garantiza que ambas zonas se conecten con gracia.
Además, el código anterior no tiene ramificaciones, lo que podría ser más eficiente. Tenga en cuenta que operaciones como max
y abs
, que a priori necesitan ramificaciones, a menudo tienen instrucciones de ensamblaje dedicadas. Por esta razón, creo que este enfoque es superior a otra solución que sería arreglar el problema de Michael nearlyEqual
cambiando el interruptor de diff < abs_th
a diff < eps * abs_th
, lo que 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 alrededor de abs_th
, 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 comparas son el resultado de una resta, quizás 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 se compara un punto flotante con 0
. En este caso, cualquier comparación relativa fracasará, porque |x - 0| / (|x| + 0) = 1
. Por lo tanto, la comparación debe cambiar al modo absoluto cuando x
sea del orden de la imprecisión de su cálculo, y rara vez es tan baja como FLT_MIN
.
Este es el motivo de la introducción del abs_th
parámetro anterior.
Además, al no multiplicar abs_th
por epsilon
, la interpretación de este parámetro es sencilla y corresponde al nivel de precisión numérica que esperamos de esos números.
Retumbar matemático
(mantenido aquí principalmente para mi propio placer)
De manera más general, supongo que un operador de comparación de punto flotante que se comporta bien =~
debería tener algunas propiedades básicas.
Los siguientes son bastante obvios:
- autoigualdad:
a =~ a
- simetría:
a =~ b
implicab =~ a
- invariancia por oposición:
a =~ b
implica-a =~ -b
(No tenemos a =~ b
e 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 punto flotante
- si
a < b < c
, entoncesa =~ c
implicaa =~ b
(los valores más cercanos también deberían ser iguales) - si
a, b, m >= 0
entoncesa =~ b
implicaa + m =~ b + m
(los valores más grandes con la misma diferencia también deberían ser iguales) - si
0 <= λ < 1
entoncesa =~ b
implicaλa =~ λb
(quizás menos obvio para argumentar a favor).
Esas propiedades ya imponen fuertes restricciones sobre posibles funciones cercanas a la igualdad. La función propuesta anteriormente los verifica. Quizás falten una o varias propiedades que de otro modo serían obvias.
Cuando se piensa en =~
una relación de familia de igualdad =~[Ɛ,t]
parametrizada por Ɛ
y abs_th
, también se podría agregar
- si
Ɛ1 < Ɛ2
entoncesa =~[Ɛ1,t] b
implicaa =~[Ɛ2,t] b
(la igualdad para una tolerancia dada implica igualdad en una tolerancia mayor) - si
t1 < t2
entoncesa =~[Ɛ,t1] b
implicaa =~[Ɛ,t2] b
(la igualdad para una imprecisión dada implica igualdad en una imprecisión mayor)
La solución propuesta también los verifica.
Comparar para mayor/menor no es realmente un problema a menos que esté trabajando justo en el borde del límite de flotación/doble precisión.
Para una comparación de "iguales difusos", esto (código Java, debería ser fácil de adaptar) es lo que se me ocurrió para La guía de punto flotante 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 pruebas. Debe descartar inmediatamente cualquier solución que no lo haga, porque está prácticamente garantizado que fallará en algunos casos extremos, como tener un valor 0, dos valores muy pequeños opuestos a cero o infinitos.
Una alternativa (consulte el enlace anterior para obtener más detalles) es convertir los patrones de bits de los flotantes a números enteros y aceptar todo dentro de una distancia entera fija.
En cualquier caso, probablemente no exista una solución que sea perfecta para todas las aplicaciones. Lo ideal sería desarrollar/adaptar el suyo propio con un conjunto de pruebas que cubra sus casos de uso reales.
Tenemos que elegir un nivel de tolerancia para comparar números flotantes. 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 sí..
Quieres decir
if (b != 1)
Console.WriteLine("Oh no!")
Idea que tuve para la comparación de punto flotante en Swift
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)
}
Adaptación a PHP de la respuesta de Michael Borgwardt y 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;
}
}
}
}
Deberías preguntarte por qué estás 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 cada contexto de aplicación. Pero en casi todos los casos prácticos se requiere una precisión absoluta . Es muy raro que se pueda aplicar una precisión relativa.
Para dar un ejemplo: si su objetivo es dibujar un gráfico en la pantalla, entonces probablemente desee que los valores de punto flotante se comparen iguales si se asignan al mismo píxel en la pantalla. Si el tamaño de su pantalla es de 1000 píxeles y sus números están en el rango de 1e6, entonces probablemente querrá comparar 100 con 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(a-b) < accuracy) // comparison wrt. the accuracy
return EQUAL;
if (a < b) // larger / smaller
return SMALLER;
else
return LARGER;
}