c++ - programacion - ¿El punto flotante== siempre está bien?
punto flotante metodos numericos (14)
Perfecto para valores integrales incluso en formatos de punto flotante
Pero la respuesta corta es: "No, no uses ==".
Irónicamente, el formato de punto flotante funciona "perfectamente", es decir, con precisión exacta, cuando se opera en valores integrales dentro del rango del formato. Esto significa que si te quedas con valores dobles , obtienes enteros perfectamente buenos con un poco más de 50 bits, lo que te da aproximadamente + - 4,500,000,000,000,000, o 4.5 cuatrillones.
De hecho, así es como funciona JavaScript internamente, y es por eso que JavaScript puede hacer cosas como +
y -
en números realmente grandes, pero solo puede <<
y >>
en los de 32 bits.
Estrictamente hablando, puede comparar exactamente sumas y productos de números con representaciones precisas. Esos serían todos los enteros, más las fracciones compuestas de 1/2 n términos. Por lo tanto, un bucle que se incremente en n + 0.25, n + 0.50 o n + 0.75 estaría bien, pero no cualquiera de las otras 96 fracciones decimales con 2 dígitos.
Entonces la respuesta es: si bien la igualdad exacta en teoría puede tener sentido en casos estrechos, es mejor evitarla.
Hoy mismo me encontré con un software de terceros que estamos usando y en su código de muestra había algo parecido a esto:
// Defined in somewhere.h
static const double BAR = 3.14;
// Code elsewhere.cpp
void foo(double d)
{
if (d == BAR)
...
}
Soy consciente del problema con los puntos flotantes y su representación, pero me hizo preguntarme si hay casos en que float == float
estaría bien. No estoy preguntando cuándo podría funcionar, sino cuándo tiene sentido y funciona.
Además, ¿qué pasa con una llamada como foo(BAR)
? ¿Esto se comparará siempre igual, ya que ambos usan la misma static const BAR
?
Digamos que tiene una función que escala una matriz de flotadores por un factor constante:
void scale(float factor, float *vector, int extent) {
int i;
for (i = 0; i < extent; ++i) {
vector[i] *= factor;
}
}
Asumiré que su implementación de punto flotante puede representar 1.0 y 0.0 exactamente, y que 0.0 está representado por todos los 0 bits.
Si el factor
es exactamente 1.0, entonces esta función no es operativa y puede regresar sin hacer ningún trabajo. Si el factor
es exactamente 0.0, entonces esto puede implementarse con una llamada a memset, que probablemente será más rápida que realizar las multiplicaciones de punto flotante individualmente.
La implementación de referencia de las funciones BLAS en netlib utiliza tales técnicas ampliamente.
El único caso en el que utilizo ==
(o !=
) Para flotadores es el siguiente:
if (x != x)
{
// Here x is guaranteed to be Not a Number
}
y debo admitir que soy culpable de usar Not A Number como una constante mágica de punto flotante (usando numeric_limits<double>::quiet_NaN()
en C ++).
No tiene sentido comparar números de punto flotante para igualdad estricta. Los números de punto flotante se han diseñado con límites de precisión relativa predecibles. Usted es responsable de saber qué precisión puede esperar de ellos y de sus algoritmos.
En mi opinión, comparar la igualdad (o alguna equivalencia) es un requisito en la mayoría de las situaciones: los contenedores o algoritmos estándar de C ++ con un functor de comparación de igualdad implícito, como std :: unordered_set, por ejemplo, requieren que este comparador sea una relación de equivalencia (ver C ++ requisitos con nombre: UnorderedAssociativeContainer ).
Desafortunadamente, comparando con un épsilon como en abs(a - b) < epsilon
no produce una relación de equivalencia ya que pierde la transitividad. Probablemente este es un comportamiento indefinido, específicamente dos números de punto flotante "casi iguales" podrían producir hashes diferentes; esto puede poner unordord_set en un estado no válido. Personalmente, usaría == para puntos flotantes la mayor parte del tiempo, a menos que cualquier tipo de cálculo de FPU esté involucrado en cualquier operando. Con contenedores y algoritmos de contenedor, donde solo están involucradas las lecturas / escrituras, == (o cualquier relación de equivalencia) es la más segura.
abs(a - b) < epsilon
es más o menos un criterio de convergencia similar a un límite. Considero que esta relación es útil si necesito verificar que una identidad matemática se mantiene entre dos cálculos (por ejemplo, PV = nRT, o distance = time * speed).
En resumen, use ==
si y solo si no se produce un cálculo de punto flotante; nunca use abs(ab) < e
como predicado de igualdad;
Hay dos formas de responder esta pregunta:
- ¿Hay casos en que
float == float
da el resultado correcto? - ¿Hay casos en que
float == float
es una codificación aceptable?
La respuesta a (1) es: Sí, a veces. Pero va a ser frágil, lo que lleva a la respuesta a (2): No. No hagas eso. Estás pidiendo bichos extraños en el futuro.
En cuanto a una llamada de la forma foo(BAR)
: En ese caso particular, la comparación se volverá verdadera, pero cuando escribe foo
no sabe (y no debería depender de) cómo se llama. Por ejemplo, llamar a foo(BAR)
estará bien, pero foo(BAR * 2.0 / 2.0)
(o incluso tal vez foo(BAR * 1.0)
dependiendo de cuánto se optimice el compilador). ¡No debe confiar en que la persona que llama no realiza ninguna operación aritmética!
En pocas palabras, a pesar de que a == b
funcionará en algunos casos, realmente no deberías confiar en ello. Incluso si puede garantizar la semántica de llamadas hoy, tal vez no pueda garantizarlas la próxima semana, así que ahórrese un poco de dolor y no use ==
.
En mi opinión, float == float
nunca es * OK porque es prácticamente imposible de mantener.
* Para pequeños valores de nunca.
Las otras publicaciones muestran donde es apropiado. Creo que usar comparaciones de bits exactos para evitar cálculos innecesarios también está bien ...
Ejemplo:
float someFunction (float argument)
{
// I really want bit-exact comparison here!
if (argument != lastargument)
{
lastargument = argument;
cachedValue = very_expensive_calculation (argument);
}
return cachedValue;
}
Las otras respuestas explican bastante bien por qué usar ==
para números de punto flotante es peligroso. Acabo de encontrar un ejemplo que ilustra estos peligros bastante bien, creo.
En la plataforma x86, puede obtener resultados de punto flotante extraños para algunos cálculos, que no se deben a problemas de redondeo inherentes a los cálculos que realiza. Este simple programa en C a veces imprime "error":
#include <stdio.h>
void test(double x, double y)
{
const double y2 = x + 1.0;
if (y != y2)
printf("error/n");
}
void main()
{
const double x = .012;
const double y = x + 1.0;
test(x, y);
}
El programa esencialmente solo calcula
x = 0.012 + 1.0;
y = 0.012 + 1.0;
(solo repartidas en dos funciones y con variables intermedias), ¡pero la comparación todavía puede producir falso!
La razón es que en la plataforma x86, los programas usualmente usan la FPU x87 para cálculos de punto flotante. El x87 calcula internamente con una precisión más alta que el double
regular, por lo que los valores double
deben redondearse cuando se almacenan en la memoria. Eso significa que un viaje de ida y vuelta x87 -> RAM -> x87 pierde precisión, y por lo tanto los resultados de los cálculos difieren dependiendo de si los resultados intermedios pasaron a través de la RAM o si todos se quedaron en los registros de FPU. Esto es, por supuesto, una decisión del compilador, por lo que el error solo se manifiesta para ciertos compiladores y configuraciones de optimización :-(.
Para obtener más información, consulte el error de GCC: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=323
Más bien aterrador ...
Nota adicional:
Los errores de este tipo generalmente serán bastante difíciles de depurar, porque los diferentes valores se vuelven iguales una vez que llegan a la memoria RAM.
Entonces, por ejemplo, si extiende el programa anterior para imprimir los patrones de bits de y
y y2
justo después de compararlos, obtendrá exactamente el mismo valor . Para imprimir el valor, debe cargarse en la RAM para pasarlo a alguna función de impresión como printf
, y eso hará que la diferencia desaparezca ...
Probablemente esté bien si nunca vas a calcular el valor antes de compararlo. Si está probando si un número de punto flotante es exactamente pi, o -1, o 1 y sabe que esos son los valores limitados que se pasan ...
Sí, tiene la garantía de que los números enteros, incluido 0.0, se comparan con ==
Por supuesto, debes tener un poco de cuidado con la forma en que obtuviste el número completo en primer lugar, la asignación es segura pero el resultado de cualquier cálculo es sospechoso
ps, hay un conjunto de números reales que tienen una reproducción perfecta como un flotador (piense en 1/2, 1/4 1/8, etc.) pero probablemente no sepa de antemano que tiene uno de estos.
Solo para aclarar. IEEE 754 garantiza que las representaciones flotantes de enteros (números enteros) dentro del rango son exactas.
float a=1.0;
float b=1.0;
a==b // true
Pero hay que tener cuidado de cómo se obtienen los números enteros
float a=1.0/3.0;
a*3.0 == 1.0 // not true !!
Sí. 1/x
será válido a menos que x==0
. No necesitas una prueba imprecisa aquí. 1/0.00000001
está perfectamente bien. No puedo pensar en ningún otro caso, ni siquiera se puede verificar tan(x)
para x==PI/2
También lo utilicé varias veces cuando reescribí algunos algoritmos en versiones de multiproceso. Utilicé una prueba que comparaba los resultados de una versión de uno o varios hilos para estar segura de que ambos dieron exactamente el mismo resultado.
Tengo un programa de dibujo que utiliza fundamentalmente un punto flotante para su sistema de coordenadas, ya que el usuario tiene permiso para trabajar en cualquier granularidad / zoom. Lo que están dibujando contiene líneas que se pueden doblar en los puntos creados por ellos. Cuando arrastran un punto encima de otro, se fusionan.
Para hacer una comparación de punto flotante "adecuada" tendría que encontrar un rango dentro del cual considerar los puntos de la misma manera. Ya que el usuario puede acercarse al infinito y trabajar dentro de ese rango y como no pude hacer que nadie se comprometiera con algún tipo de rango, solo usamos ''=='' para ver si los puntos son iguales. Ocasionalmente, habrá un problema en el que los puntos que supuestamente deben ser exactamente iguales están desactivados en .000000000001 o algo así (especialmente alrededor de 0,0), pero generalmente funciona bien. Se supone que es difícil fusionar puntos sin el complemento activado de todos modos ... o al menos así es como funcionó la versión original.
Se arroja del grupo de prueba ocasionalmente pero ese es su problema: p
De todos modos, hay un ejemplo de un tiempo posiblemente razonable para usar ''==''. Lo que hay que tener en cuenta es que la decisión es menos sobre la precisión técnica que sobre los deseos del cliente (o la falta de ellos) y la conveniencia. No es algo que tenga que ser tan preciso de todos modos. Entonces, ¿qué pasa si dos puntos no se fusionan cuando esperas que lo hagan? No es el fin del mundo y no afectará a los ''cálculos''.
Trataré de proporcionar ejemplos más o menos reales de pruebas legítimas, significativas y útiles para la igualdad de flotación.
#include <stdio.h>
#include <math.h>
/* let''s try to numerically solve a simple equation F(x)=0 */
double F(double x) {
return 2*cos(x) - pow(1.2, x);
}
/* I''ll use a well-known, simple&slow but extremely smart method to do this */
double bisection(double range_start, double range_end) {
double a = range_start;
double d = range_end - range_start;
int counter = 0;
while(a != a+d) // <-- WHOA!!
{
d /= 2.0;
if(F(a)*F(a+d) > 0) /* test for same sign */
a = a+d;
++counter;
}
printf("%d iterations done/n", counter);
return a;
}
int main() {
/* we must be sure that the root can be found in [0.0, 2.0] */
printf("F(0.0)=%.17f, F(2.0)=%.17f/n", F(0.0), F(2.0));
double x = bisection(0.0, 2.0);
printf("the root is near %.17f, F(%.17f)=%.17f/n", x, x, F(x));
}
Prefiero no explicar el método de bisección utilizado en sí mismo, pero enfatizar en la condición de parada. Tiene exactamente la forma comentada: (a == a+d)
donde ambos lados son flotadores: a
es nuestra aproximación actual de la raíz de la ecuación, d
es nuestra precisión actual. Dada la condición previa del algoritmo, que debe haber una raíz entre range_start
y range_end
, garantizamos en cada iteración que la raíz se mantenga entre a
y a+d
mientras que d
se reduce a la mitad en cada paso, reduciendo los límites.
Y luego, después de una serie de iteraciones, ¡ d
vuelve tan pequeño que durante la suma con a
se redondea a cero! Es decir, a+d
resulta estar más cerca de a
flotador que de cualquier otro ; y así, la FPU lo redondea al valor más cercano: a la a
sí misma. Esto se puede ilustrar fácilmente mediante el cálculo en una máquina de cálculo hipotética; Deje que tenga una mantisa decimal de 4 dígitos y algún rango de exponente grande. Entonces, ¿qué resultado debe dar la máquina a 2.131e+02 + 7.000e-3
? La respuesta exacta es 213.107
, pero nuestra máquina no puede representar tal número; Tiene que redondearlo. Y 213.107
está mucho más cerca de 213.1
que de 213.2
, por lo que el resultado redondeado se convierte en 2.131e+02
, el pequeño sumando desapareció, redondeado a cero. Se garantiza que exactamente lo mismo sucederá en alguna iteración de nuestro algoritmo, y en ese momento ya no podemos continuar. Hemos encontrado la raíz a la máxima precisión posible.
La conclusión edificante es, aparentemente, que las flotaciones son difíciles. Se parecen tanto a los números reales que todo programador se siente tentado a pensar en ellos como números reales. Pero no lo son. Tienen su propio comportamiento, que recuerda ligeramente al de los reales , pero no del mismo modo. Debe ser muy cuidadoso con ellos, especialmente cuando se compara con la igualdad.
Actualizar
Revisando la respuesta después de un tiempo, también he notado un hecho interesante: en el algoritmo anterior no se puede usar "un número pequeño" en la condición de parada. Para cualquier elección del número, habrá entradas que considerarán su elección demasiado grande , causando pérdida de precisión, y habrá entradas que considerarán su elección demasiado pequeña , causando un exceso de iteraciones o incluso ingresando a un bucle infinito. La discusión detallada sigue.
Es posible que ya sepa que el cálculo no tiene noción de un "número pequeño": para cualquier número real, puede encontrar fácilmente un número infinito de números incluso más pequeños. El problema es que uno de esos "incluso más pequeños" podría ser lo que realmente buscamos; Podría ser una raíz de nuestra ecuación. Peor aún, para diferentes ecuaciones puede haber raíces distintas (por ejemplo, 2.51e-8
y 1.38e-8
), ambas se aproximarán por el mismo número si nuestra condición de detención se vería como d < 1e-6
. Sea cual sea el "número pequeño" que elija, muchas raíces que se encontrarán correctamente con la máxima precisión con a == a+d
condición de detención a == a+d
se dañarán si el "épsilon" es demasiado grande .
Sin embargo, es cierto que en los números de punto flotante el exponente tiene un rango limitado, por lo que puede encontrar el número de FP positivo distinto de cero más pequeño (por ejemplo, 1e-45
denorm para la precisión única FP IEEE 754). ¡Pero es inútil! while (d < 1e-45) {...}
repetirá para siempre, asumiendo una precisión simple (positivo a cero) d
.
Dejando a un lado los casos de borde patológico, cualquier elección del "número pequeño" en la condición de parada d < eps
será demasiado pequeña para muchas ecuaciones. En aquellas ecuaciones donde la raíz tiene el exponente lo suficientemente alto, el resultado de la sustracción de dos mantisas que difieren solo en el dígito menos significativo fácilmente excederá nuestro "épsilon". Por ejemplo, con 7.00023e+8 - 7.00022e+8 = 0.00001e+8 = 1.00000e+3 = 1000
6 dígitos 7.00023e+8 - 7.00022e+8 = 0.00001e+8 = 1.00000e+3 = 1000
, lo que significa que la diferencia más pequeña posible entre números con exponente +8 y mantisa de 5 dígitos es .. 1000! Que nunca encajará en, digamos, 1e-4
. Para estos números con exponente (relativamente) alto simplemente no tenemos la precisión suficiente para ver una diferencia de 1e-4
.
Mi implementación anterior también tomó en cuenta este último problema, y se puede ver que d
se divide en dos en cada paso, en lugar de recalcularse como una diferencia de (posiblemente enorme en exponente) a
y b
. Entonces, si cambiamos la condición de parada a d < eps
, el algoritmo no se quedará atascado en un bucle infinito con raíces enormes (podría muy bien con (ba) < eps
), pero seguirá realizando iteraciones innecesarias durante la reducción d
abajo. precisión de a
.
Este tipo de razonamiento puede parecer demasiado teórico y innecesariamente profundo, pero su propósito es ilustrar de nuevo la delicadeza de los flotadores. Se debe tener mucho cuidado con su precisión finita cuando se escriben operadores aritméticos a su alrededor.
Yo diría que la comparación de flotadores para la igualdad estaría bien si una respuesta falsa negativa es aceptable .
Supongamos, por ejemplo, que tiene un programa que imprime valores de puntos flotantes en la pantalla y que si el valor de punto flotante es exactamente igual a M_PI
, le gustaría que imprima "pi" en su lugar. Si el valor se desvía un poco de la representación doble exacta de M_PI
, se imprimirá un valor doble, que es igualmente válido, pero un poco menos legible para el usuario.