¿Qué tan peligroso es comparar valores de coma flotante?

391

UIKitusos CGFloatdebido a la resolución del sistema de coordenadas independiente.

Pero cada vez que quiere comprobar si por ejemplo frame.origin.xse 0me hace sentir enferma:

if (theView.frame.origin.x == 0) {
    // do important operation
}

No es CGFloatvulnerable a los falsos positivos cuando se compara con ==, <=, >=, <, >? Es un punto flotante y tienen problemas de precisión: 0.0000000000041por ejemplo.

¿Está Objective-Cmanejando esto internamente cuando se compara o puede suceder que un origin.xque se lee como cero no se compara 0como verdadero?

Miembro orgulloso
fuente

Respuestas:

466

En primer lugar, los valores de coma flotante no son "aleatorios" en su comportamiento. La comparación exacta puede y tiene sentido en muchos usos del mundo real. Pero si va a usar coma flotante, debe saber cómo funciona. Errar al lado de asumir que el punto flotante funciona como números reales le dará un código que se rompe rápidamente. Errar al lado de suponer que los resultados de coma flotante tienen una gran confusión aleatoria asociada con ellos (como la mayoría de las respuestas aquí sugieren) obtendrá un código que parece funcionar al principio, pero termina teniendo errores de gran magnitud y casos de esquinas rotas.

En primer lugar, si desea programar con coma flotante, debe leer esto:

Lo que todo informático debe saber sobre la aritmética de coma flotante

Sí, léelo todo. Si es una carga demasiado pesada, debe usar enteros / punto fijo para sus cálculos hasta que tenga tiempo de leerlo. :-)

Ahora, dicho esto, los mayores problemas con las comparaciones exactas de coma flotante se reducen a:

  1. El hecho de que muchos valores que puede escribir en la fuente, o leer con scanfo strtod, no existan como valores de coma flotante y se conviertan silenciosamente a la aproximación más cercana. De esto es de lo que hablaba la respuesta de demon9733.

  2. El hecho de que muchos resultados se redondeen debido a que no tienen la precisión suficiente para representar el resultado real. Un ejemplo fácil donde puedes ver esto es agregar x = 0x1fffffey y = 1como flotantes. Aquí, xtiene 24 bits de precisión en la mantisa (ok) y ysolo tiene 1 bit, pero cuando los agrega, sus bits no están en lugares superpuestos, y el resultado necesitaría 25 bits de precisión. En cambio, se redondea ( 0x2000000en el modo de redondeo predeterminado).

  3. El hecho de que muchos resultados se redondeen debido a la necesidad de infinitos lugares para obtener el valor correcto. Esto incluye resultados racionales como 1/3 (con el que está familiarizado desde el decimal, donde toma infinitos lugares), pero también 1/10 (que también toma infinitos lugares en binario, ya que 5 no es una potencia de 2), así como resultados irracionales como la raíz cuadrada de cualquier cosa que no sea un cuadrado perfecto.

  4. Doble redondeo. En algunos sistemas (particularmente x86), las expresiones de coma flotante se evalúan con mayor precisión que sus tipos nominales. Esto significa que cuando ocurre uno de los tipos de redondeo anteriores, obtendrá dos pasos de redondeo, primero un redondeo del resultado al tipo de mayor precisión, luego un redondeo al tipo final. Como ejemplo, considere lo que sucede en decimal si redondea 1.49 a un entero (1), en comparación con lo que sucede si primero lo redondea a un decimal (1.5) y luego redondea ese resultado a un entero (2). Esta es en realidad una de las áreas más desagradables para tratar en coma flotante, ya que el comportamiento del compilador (especialmente para compiladores defectuosos y no conformes como GCC) es impredecible.

  5. Funciones trascendentales ( trig, exp, log, etc.) no se especifican para tener resultados correctamente redondeados; el resultado solo se especifica como correcto dentro de una unidad en el último lugar de precisión (generalmente denominado 1ulp ).

Cuando escribe código de coma flotante, debe tener en cuenta lo que está haciendo con los números que podrían causar que los resultados sean inexactos, y hacer comparaciones en consecuencia. Muchas veces tendrá sentido comparar con un "épsilon", pero ese épsilon debe basarse en la magnitud de los números que está comparando , no en una constante absoluta. (En los casos en que una épsilon constante constante funcionaría, ¡eso es muy indicativo de que el punto fijo, no el punto flotante, es la herramienta adecuada para el trabajo!)

Editar: en particular, una verificación de épsilon relativa a la magnitud debería ser similar a:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))

Donde FLT_EPSILONes la constante de float.h(reemplazarla con DBL_EPSILONpor doubles o LDBL_EPSILONde long doubles) y Kes una constante que elija de tal manera que el error acumulado de sus cálculos es, sin duda limitado por Klas unidades en el último lugar (y si no está seguro de que tiene el error cálculo correcto a la derecha, haga Kunas veces más grande de lo que sus cálculos dicen que debería ser).

Finalmente, tenga en cuenta que si usa esto, es posible que necesite un cuidado especial cerca de cero, ya FLT_EPSILONque no tiene sentido para los denormales. Una solución rápida sería hacerlo:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)

y del mismo modo sustituir DBL_MINsi se usan dobles.

R .. GitHub DEJA DE AYUDAR AL HIELO
fuente
25
fabs(x+y)es problemático si xy y(puede) tener un signo diferente. Aún así, una buena respuesta contra la corriente de las comparaciones de culto de carga.
Daniel Fischer
27
Si xy ytiene un signo diferente, no hay problema. El lado derecho será "demasiado pequeño", pero desde xy ytienen distinto signo, que no debe comparar la igualdad de todos modos. (A menos que sean tan pequeños como para ser denormales, pero luego el segundo caso lo atrapa)
R .. GitHub DEJA DE AYUDAR AL HIELO
44
Tengo curiosidad acerca de su declaración: "especialmente para compiladores con errores, no conformes como GCC". ¿Es realmente defectuoso el CCG y también no es conforme?
Nicolás Ozimica
3
Dado que la pregunta está etiquetada como iOS, vale la pena señalar que los compiladores de Apple (tanto las compilaciones de clang como las de gcc de Apple) siempre han usado FLT_EVAL_METHOD = 0, e intentan ser completamente estrictos para no tener un exceso de precisión. Si encuentra alguna violación de eso, presente informes de errores.
Stephen Canon
17
"En primer lugar, los valores de coma flotante no son" aleatorios "en su comportamiento. La comparación exacta puede y tiene sentido en muchos usos del mundo real". - ¡Solo dos oraciones y ya obtuve un +1! Esa es una de las suposiciones erróneas más inquietantes que hacen las personas cuando trabajan con puntos flotantes.
Christian Rau
36

Dado que 0 es exactamente representable como un número de punto flotante IEEE754 (o usando cualquier otra implementación de números fp con los que he trabajado), la comparación con 0 es probablemente segura. Sin embargo, podría ser mordido si su programa calcula un valor (como theView.frame.origin.x) que tiene razones para creer que debería ser 0 pero que su cálculo no puede garantizar que sea 0.

Para aclarar un poco, un cálculo como:

areal = 0.0

(a menos que su idioma o sistema esté roto) creará un valor tal que (areal == 0.0) devuelve verdadero pero otro cálculo como

areal = 1.386 - 2.1*(0.66)

podría no.

Si puede asegurarse de que sus cálculos producen valores que son 0 (y no solo que producen valores que deberían ser 0), entonces puede continuar y comparar los valores de fp con 0. Si no puede asegurarse del grado requerido , mejor se adhieren al enfoque habitual de "igualdad tolerada".

En los peores casos, la comparación descuidada de los valores de fp puede ser extremadamente peligrosa: piense en aviónica, orientación de armas, operaciones de plantas de energía, navegación de vehículos, casi cualquier aplicación en la que la computación se encuentre con el mundo real.

Para Angry Birds, no es tan peligroso.

Marca de alto rendimiento
fuente
11
En realidad, 1.30 - 2*(0.65)es un ejemplo perfecto de una expresión que obviamente evalúa a 0.0 si su compilador implementa IEEE 754, porque los dobles representados como 0.65y 1.30tienen los mismos significados, y la multiplicación por dos es obviamente exacta.
Pascal Cuoq
77
Todavía obtengo representante de este, así que cambié el segundo fragmento de ejemplo.
Alto rendimiento Mark
22

Quiero dar una respuesta un poco diferente a las demás. Son excelentes para responder a su pregunta como se indicó, pero probablemente no para lo que necesita saber o cuál es su verdadero problema.

¡El punto flotante en los gráficos está bien! Pero casi no hay necesidad de comparar flotadores directamente. ¿Por qué necesitarías hacer eso? Los gráficos utilizan flotadores para definir intervalos. ¡Y comparar si un flotador está dentro de un intervalo también definido por flotadores siempre está bien definido y simplemente necesita ser consistente, no exacto o preciso! Siempre y cuando se pueda asignar un píxel (¡que también es un intervalo!), Eso es todo lo que necesita el gráfico.

Entonces, si desea probar si su punto está fuera de un rango de [0..ancho [esto está bien. Solo asegúrese de definir la inclusión de manera consistente. Por ejemplo, siempre definir dentro es (x> = 0 && x <ancho). Lo mismo ocurre con las pruebas de intersección o acierto.

Sin embargo, si abusa de una coordenada gráfica como algún tipo de indicador, como por ejemplo para ver si una ventana está acoplada o no, no debe hacerlo. Utilice una bandera booleana que esté separada de la capa de presentación de gráficos en su lugar.

Starmole
fuente
13

Comparar con cero puede ser una operación segura, siempre que el cero no sea un valor calculado (como se señaló en la respuesta anterior). La razón de esto es que cero es un número perfectamente representable en coma flotante.

Hablando de valores perfectamente representables, obtienes 24 bits de rango en una noción de potencia de dos (precisión simple). Entonces 1, 2, 4 son perfectamente representables, como lo son .5, .25 y .125. Mientras todos sus bits importantes estén en 24 bits, usted es dorado. Entonces 10.625 se puede representar con precisión.

Esto es genial, pero se desmoronará rápidamente bajo presión. Se me ocurren dos escenarios: 1) Cuando se trata de un cálculo. No confíes en sqrt (3) * sqrt (3) == 3. Simplemente no será así. Y probablemente no estará dentro de un épsilon, como sugieren algunas de las otras respuestas. 2) Cuando está involucrado cualquier no poder de 2 (NPOT). Por lo tanto, puede sonar extraño, pero 0.1 es una serie infinita en binario y, por lo tanto, cualquier cálculo que implique un número como este será impreciso desde el principio.

(Ah, y la pregunta original mencionaba las comparaciones con cero. No olvide que -0.0 también es un valor de punto flotante perfectamente válido).

JHumphrey
fuente
11

[La 'respuesta correcta' pasa por alto la selección K. Seleccionar Ktermina siendo tan ad-hoc como seleccionar, VISIBLE_SHIFTpero seleccionar Kes menos obvio porque, a diferencia de VISIBLE_SHIFTesto, no se basa en ninguna propiedad de visualización. Por lo tanto, elija su veneno: seleccione Ko seleccione VISIBLE_SHIFT. Esta respuesta aboga por seleccionar VISIBLE_SHIFTy luego demuestra la dificultad de seleccionar K]

Precisamente debido a errores redondos, no debe usar la comparación de valores 'exactos' para operaciones lógicas. En su caso específico de una posición en una pantalla visual, no puede importar si la posición es 0.0 o 0.0000000003; la diferencia es invisible para el ojo. Entonces su lógica debería ser algo como:

#define VISIBLE_SHIFT    0.0001        // for example
if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }

Sin embargo, al final, 'invisible a la vista' dependerá de las propiedades de su pantalla. Si puede hacer un límite superior de la pantalla (debería poder hacerlo); luego elige VISIBLE_SHIFTser una fracción de ese límite superior.

Ahora, la "respuesta correcta" se basa Kasí que exploremos la selección K. La 'respuesta correcta' anterior dice:

K es una constante que elige de tal manera que el error acumulado de sus cálculos está definitivamente limitado por K unidades en último lugar (y si no está seguro de haber acertado el cálculo del límite de error, haga que K sea un poco más grande de lo que sus cálculos decir que debería ser)

Entonces lo necesitamos K. Si obtener Kes más difícil, menos intuitivo que seleccionar mi, VISIBLE_SHIFTentonces usted decidirá qué funciona para usted. Para encontrarlo K, vamos a escribir un programa de prueba que Kobserve un conjunto de valores para que podamos ver cómo se comporta. Debería ser obvio cómo elegir K, si la "respuesta correcta" es utilizable. ¿No?

Vamos a utilizar, como los detalles de 'respuesta correcta':

if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)

Probemos con todos los valores de K:

#include <math.h>
#include <float.h>
#include <stdio.h>

void main (void)
{
  double x = 1e-13;
  double y = 0.0;

  double K = 1e22;
  int i = 0;

  for (; i < 32; i++, K = K/10.0)
    {
      printf ("K:%40.16lf -> ", K);

      if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)
        printf ("YES\n");
      else
        printf ("NO\n");
    }
}
ebg@ebg$ gcc -o test test.c
ebg@ebg$ ./test
K:10000000000000000000000.0000000000000000 -> YES
K: 1000000000000000000000.0000000000000000 -> YES
K:  100000000000000000000.0000000000000000 -> YES
K:   10000000000000000000.0000000000000000 -> YES
K:    1000000000000000000.0000000000000000 -> YES
K:     100000000000000000.0000000000000000 -> YES
K:      10000000000000000.0000000000000000 -> YES
K:       1000000000000000.0000000000000000 -> NO
K:        100000000000000.0000000000000000 -> NO
K:         10000000000000.0000000000000000 -> NO
K:          1000000000000.0000000000000000 -> NO
K:           100000000000.0000000000000000 -> NO
K:            10000000000.0000000000000000 -> NO
K:             1000000000.0000000000000000 -> NO
K:              100000000.0000000000000000 -> NO
K:               10000000.0000000000000000 -> NO
K:                1000000.0000000000000000 -> NO
K:                 100000.0000000000000000 -> NO
K:                  10000.0000000000000000 -> NO
K:                   1000.0000000000000000 -> NO
K:                    100.0000000000000000 -> NO
K:                     10.0000000000000000 -> NO
K:                      1.0000000000000000 -> NO
K:                      0.1000000000000000 -> NO
K:                      0.0100000000000000 -> NO
K:                      0.0010000000000000 -> NO
K:                      0.0001000000000000 -> NO
K:                      0.0000100000000000 -> NO
K:                      0.0000010000000000 -> NO
K:                      0.0000001000000000 -> NO
K:                      0.0000000100000000 -> NO
K:                      0.0000000010000000 -> NO

Ah, entonces K debería ser 1e16 o mayor si quiero que 1e-13 sea 'cero'.

Entonces, diría que tienes dos opciones:

  1. Haga un cálculo simple de epsilon utilizando su criterio de ingeniería para el valor de 'epsilon', como he sugerido. Si está haciendo gráficos y 'cero' está destinado a ser un 'cambio visible' que examine sus activos visuales (imágenes, etc.) y juzgue qué puede ser épsilon.
  2. No intente ningún cálculo de coma flotante hasta que haya leído la referencia de la respuesta que no es de culto de carga (y haya obtenido su doctorado en el proceso) y luego use su juicio no intuitivo para seleccionar K.
GoZoner
fuente
10
Un aspecto de la resolución-independencia es que no se puede saber con certeza qué es un "cambio visible" en tiempo de compilación. Lo que es invisible en una pantalla súper HD bien podría ser obvio en una pantalla de culo pequeño. Al menos uno debería hacer que sea una función del tamaño de la pantalla. O nombrarlo de otra manera.
Romain
1
Pero al menos seleccionar 'desplazamiento visible' se basa en propiedades de visualización (o marco) fáciles de entender, a diferencia de las <respuestas correctas> Kque es difícil y no intuitivo de seleccionar.
GoZoner
5

La pregunta correcta: ¿cómo se comparan los puntos en Cocoa Touch?

La respuesta correcta: CGPointEqualToPoint ().

Una pregunta diferente: ¿Son dos valores calculados iguales?

La respuesta publicada aquí: no lo son.

¿Cómo verificar si están cerca? Si desea verificar si están cerca, no use CGPointEqualToPoint (). Pero no verifique si están cerca. Haga algo que tenga sentido en el mundo real, como verificar si un punto está más allá de una línea o si un punto está dentro de una esfera.

Michael T.
fuente
4

La última vez que verifiqué el estándar C, no era necesario que las operaciones de coma flotante en dobles (64 bits en total, mantisa de 53 bits) fueran más precisas que esa precisión. Sin embargo, parte del hardware podría realizar las operaciones en registros de mayor precisión, y el requisito se interpretó en el sentido de que no requiere borrar bits de orden inferior (más allá de la precisión de los números que se cargan en los registros). Por lo tanto, podría obtener resultados inesperados de comparaciones como esta dependiendo de lo que quedó en los registros de quien durmió allí por última vez.

Dicho esto, y a pesar de mis esfuerzos por eliminarlo cada vez que lo veo, el equipo donde trabajo tiene mucho código C que se compila usando gcc y se ejecuta en Linux, y no hemos notado ninguno de estos resultados inesperados en mucho tiempo . No tengo idea de si esto se debe a que gcc está borrando los bits de bajo orden para nosotros, los registros de 80 bits no se usan para estas operaciones en computadoras modernas, el estándar ha cambiado, o qué. Me gustaría saber si alguien puede citar capítulos y versos.

Membrana Lucas
fuente
1

Puede usar dicho código para comparar flotante con cero:

if ((int)(theView.frame.origin.x * 100) == 0) {
    // do important operation
}

Esto se comparará con una precisión de 0.1, suficiente para CGFloat en este caso.

Igor
fuente
Lanzar intsin asegurar theView.frame.origin.xestá dentro / cerca de ese rango de intconductores a un comportamiento indefinido (UB), o en este caso, 1/100 del rango de int.
chux - Restablece a Mónica el
No hay absolutamente ninguna razón para convertir a un entero como este. Como dijo chux, existe el potencial de UB a partir de valores fuera de rango; y en algunas arquitecturas esto será significativamente más lento que simplemente hacer el cálculo en coma flotante. Por último, multiplicar por 100 de esa manera se comparará con una precisión de 0.01, no con 0.1.
Sneftel
0
-(BOOL)isFloatEqual:(CGFloat)firstValue secondValue:(CGFloat)secondValue{

BOOL isEqual = NO;

NSNumber *firstValueNumber = [NSNumber numberWithDouble:firstValue];
NSNumber *secondValueNumber = [NSNumber numberWithDouble:secondValue];

isEqual = [firstValueNumber isEqualToNumber:secondValueNumber];

return isEqual;

}

Abbas Mulani
fuente
0

Estoy usando la siguiente función de comparación para comparar varios lugares decimales:

bool compare(const double value1, const double value2, const int precision)
{
    int64_t magnitude = static_cast<int64_t>(std::pow(10, precision));
    int64_t intValue1 = static_cast<int64_t>(value1 * magnitude);
    int64_t intValue2 = static_cast<int64_t>(value2 * magnitude);
    return intValue1 == intValue2;
}

// Compare 9 decimal places:
if (compare(theView.frame.origin.x, 0, 9)) {
    // do important operation
}
mezclilla
fuente
-6

Yo diría que lo correcto es declarar cada número como un objeto, y luego definir tres cosas en ese objeto: 1) un operador de igualdad. 2) un método setAcceptableDifference. 3) el valor en sí. El operador de igualdad devuelve verdadero si la diferencia absoluta de dos valores es menor que el valor establecido como aceptable.

Puede subclasificar el objeto para adaptarlo al problema. Por ejemplo, las barras de metal redondas de entre 1 y 2 pulgadas pueden considerarse de igual diámetro si sus diámetros difieren en menos de 0.0001 pulgadas. Entonces llamaría a setAcceptableDifference con el parámetro 0.0001, y luego usaría el operador de igualdad con confianza.

John White
fuente
1
Esta no es una buena respuesta. Primero, todo el "objeto" no hace nada para resolver su problema. Y segundo, su implementación real de "igualdad" no es, de hecho, la correcta.
Tom Swirly
3
Tom, tal vez pienses de nuevo en el "objeto". Con números reales, representados con alta precisión, la igualdad rara vez ocurre. Pero la idea de igualdad de uno puede adaptarse si le conviene. Sería mejor si hubiera un operador reemplazable 'aproximadamente igual', pero no lo hay.
John White