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, al tiempo que es potencialmente más eficiente.
- Conozca la imprecisión esperada que tiene en sus números y aliméntelos 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)
{
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());
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 x
y y
se considera relativa a su amplitud |x| + |y|
. Cuando se traza en 2D, da el siguiente perfil, donde verde significa igualdad de x
y y
. (Tomé una epsilon
de 0.5 con fines ilustrativos).
El modo relativo es lo que se utiliza 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 (nuevamente con un epsilon
0,5 y un relth
1 para la ilustración).
Este modo absoluto de comparación es el que se utiliza para valores de coma flotante "diminutos".
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 relth
( Float.MIN_NORMAL
en su respuesta). Esta zona de cambio se muestra sombreada en el siguiente gráfico.
Debido a que relth * epsilon
es más pequeño que relth
, los parches verdes no se pegan, lo que a su vez le da a la solución una mala propiedad: podemos encontrar tripletes de números como ese x < y_1 < y_2
y aún x == y2
pero x != y1
.
Tome 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 cambio se basa en el valor de |x| + |y|
, que está representado por el cuadro sombreado a continuación. Asegura que ambas zonas se conecten con elegancia.
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 ramificación, suelen tener instrucciones de montaje dedicadas. Por esta razón, creo que este enfoque es superior a otra solución que sería arreglar el de Michael nearlyEqual
cambiando el interruptor de diff < relth
a diff < eps * relth
, 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 relth
, lo 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, 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 consideras comparar un punto flotante con 0
. Aquí, cualquier comparación relativa fallará, porque |x - 0| / (|x| + 0) = 1
. Por lo tanto, la comparación debe cambiar al modo absoluto cuando x
está en el orden de la imprecisión de su cálculo, y rara vez es tan bajo como FLT_MIN
.
Este es el motivo de la introducción del relth
parámetro 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 de esos números.
Ruido matemático
(guardado aquí principalmente para mi propio placer)
De manera más general, supongo que un operador de comparación de punto flotante con buen comportamiento =~
debería tener algunas propiedades básicas.
Los siguientes son bastante obvios:
- auto-igualdad:
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
, entonces a =~ c
implica a =~ b
(los valores más cercanos también deben ser iguales)
- si
a, b, m >= 0
entonces a =~ b
implica a + m =~ b + m
(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 dan fuertes restricciones sobre posibles funciones de casi igualdad. La función propuesta anteriormente los verifica. Quizás falten una o varias propiedades obvias.
Cuando se piensa =~
en una relación de familia 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 para una tolerancia más alta)
- si
t1 < t2
entonces a =~[Ɛ,t1] b
implica a =~[Ɛ,t2] b
(igualdad para una imprecisión dada implica igualdad en una imprecisión mayor)
La solución propuesta también verifica estos.