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

Sé que UIKit usa CGFloat debido al sistema de coordenadas independiente de la resolución.

Pero cada vez que quiero comprobar si, por ejemplo, frame.origin.x es 0 , me hace sentir frame.origin.x :

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

¿ CGFloat no CGFloat vulnerable a falsos positivos cuando se compara con == , <= , >= , < , > ? Es un punto flotante y tienen problemas de precisión: 0.0000000000041 por ejemplo.

¿ Objective-C maneja esto internamente al comparar o puede ocurrir que un origin.x que se lea como cero no se compare con 0 como verdadero?

En primer lugar, los valores de punto flotante no son “aleatorios” en su comportamiento. La comparación exacta puede tener y tiene sentido en muchos usos del mundo real. Pero si vas a usar el punto flotante, debes ser consciente de cómo funciona. Errar del lado de asumir que el punto flotante funciona como números reales obtendrá un código que se rompe rápidamente. Errar al suponer que los resultados en coma flotante tienen una fuzz aleatoria grande asociada (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 esquina rotos.

Antes que nada, si quieres progtwigr con coma flotante, deberías leer esto:

Lo que todo informático debería saber sobre la aritmética de coma flotante

Sí, léelo todo. Si eso es una carga excesiva, debe usar enteros / punto fijo para sus cálculos hasta que tenga tiempo para leerlo. 🙂

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

  1. El hecho de que muchos de los valores que puede escribir en la fuente, o leer con scanf o strtod , no existen como valores de coma flotante y se convierten silenciosamente a la aproximación más cercana. De esto es de lo que estaba hablando la respuesta de demon9733.

  2. El hecho de que muchos resultados se redondean debido a que no tiene la precisión suficiente para representar el resultado real. Un ejemplo fácil en el que puede ver esto es agregar x = 0x1fffffe y y = 1 como flotantes. Aquí, x tiene 24 bits de precisión en la mantisa (ok) e y tiene solo 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 (a 0x2000000 en el modo de redondeo predeterminado).

  3. El hecho de que muchos resultados se redondean debido a la necesidad de infinitos lugares para obtener el valor correcto. Esto incluye resultados racionales como 1/3 (que está familiarizado con 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 se produce uno de los tipos de redondeos 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), frente a lo que sucede si lo redondea primero a un lugar decimal (1.5) y luego redondea ese resultado a un número entero (2). Esta es en realidad una de las áreas más desagradables para tratar en coma flotante, ya que el comportamiento del comstackdor (especialmente para comstackdores defectuosos y no conformes como GCC) es impredecible.

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

Cuando está escribiendo código de punto 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 debería basarse en la magnitud de los números que está comparando , no en una constante absoluta. (En los casos en que un épsilon constante absoluto 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 épsilon relativa a la magnitud debería ser algo así como:

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

Donde FLT_EPSILON es la constante de float.h (reemplázalo con DBL_EPSILON por double s o LDBL_EPSILON por long double s long double ) y K es una constante que eliges de modo que el error acumulado de tus cálculos está definitivamente limitado por K unidades en el último lugar ( y si no está seguro de haber obtenido el cálculo correcto de errores, haga que K un poco 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 se necesite un cuidado especial cerca de cero, ya que FLT_EPSILON no tiene sentido para los denormales. Una solución rápida sería hacerlo:

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

y también sustituye DBL_MIN si usa dobles.

Como 0 es exactamente representable como un número de coma flotante IEEE754 (o usando cualquier otra implementación de números fp con los que haya trabajado alguna vez), la comparación con 0 probablemente sea segura. Sin embargo, es posible que lo theView.frame.origin.x si su progtwig calcula un valor (como theView.frame.origin.x ) que tiene razones para creer que debería ser 0 pero cuyo cálculo no puede garantizar que sea 0.

Para aclarar un poco, un cálculo como:

 areal = 0.0 

will (a menos que su idioma o sistema esté roto) crea 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 puedes asegurarte que tus cálculos producen valores que son 0 (y no solo que produzcan valores que deberían ser 0), entonces puedes avanzar y comparar los valores de fp con 0. Si no puedes asegurarte el grado requerido , mejor ceñirse al enfoque habitual de ‘igualdad tolerada’.

En el peor de los casos, la comparación descuidada de los valores de fp puede ser extremadamente peligrosa: piense en aviónica, guiado de armas, operaciones en la planta de energía, navegación de vehículos, casi cualquier aplicación en la que el cálculo se una con el mundo real.

Para Angry Birds, no es tan peligroso.

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

¡El punto flotante en gráficos está bien! Pero casi no hay necesidad de comparar flotadores directamente. ¿Por qué necesitarías hacer eso? Graphics usa 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 preciso ni preciso! Siempre que se pueda asignar un píxel (¡que también es un intervalo!), Eso es lo que todas las necesidades de gráficos.

Entonces, si quieres probar si tu punto está fuera de un rango de [0..width], está bien. Solo asegúrese de definir la inclusión de manera consistente. Por ejemplo, siempre definir adentro es (x> = 0 && x

Sin embargo, si está abusando de una coordenada de gráficos como un tipo de bandera, como por ejemplo para ver si una ventana está acoplada o no, no debe hacer esto. Utilice un indicador booleano que sea independiente de la capa de presentación de gráficos.

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

Hablando 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. Siempre que todos tus bits importantes estén en 24 bits, estás dorado. Entonces, 10.625 puede repetirse con precisión.

Esto es genial, pero se derrumbará rápidamente bajo presión. Dos escenarios vienen a la mente: 1) Cuando se trata de un cálculo. No confíes en que 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 un 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 menciona comparaciones a cero. No olvide que -0.0 también es un valor de coma flotante perfectamente válido).

[La ‘respuesta correcta’ pasa por alto al seleccionar K Seleccionar K termina siendo tan ad hoc como seleccionar VISIBLE_SHIFT pero seleccionar K es menos obvio porque a diferencia de VISIBLE_SHIFT no se basa en ninguna propiedad de visualización. Por lo tanto, elija su veneno: seleccione K o seleccione VISIBLE_SHIFT . Esta respuesta recomienda seleccionar VISIBLE_SHIFT y 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, posiblemente no importa si la posición es 0.0 o 0.0000000003 – la diferencia es invisible al ojo. Entonces tu lógica debería ser algo así como:

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

Sin embargo, al final, 'invisible para el ojo' dependerá de sus propiedades de visualización. Si puedes enlazar la pantalla superior (deberías poder); luego elija VISIBLE_SHIFT para que sea una fracción de ese límite superior.

Ahora, la 'respuesta correcta' se basa en K así que vamos a explorar la elección de K La 'respuesta correcta' anterior dice:

K es una constante que eliges de modo que el error acumulado de tus cálculos está definitivamente limitado por K unidades en el último lugar (y si no estás seguro de que obtuviste el cálculo de límite de error correcto, haz K unas veces más grande que tus cálculos di que debería ser)

Entonces necesitamos K Si obtener K es más difícil, menos intuitivo que seleccionar mi VISIBLE_SHIFT entonces usted decidirá lo que funciona para usted. Para encontrar K , vamos a escribir un progtwig de prueba que vea un montón de valores K 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 usar, como la 'respuesta correcta' detalles:

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

Probemos todos los valores de K:

 #include  #include  #include  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(xy) < K * DBL_EPSILON * fabs(x+y) || fabs(xy) < 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 más grande si quiero que 1e-13 sea 'cero'.

Entonces, diría que tienes dos opciones:

  1. Haga un cálculo épsilon simple utilizando su juicio de ingeniería para el valor de 'épsilon', como he sugerido. Si está haciendo gráficos y 'cero' pretende ser un 'cambio visible' que examinar sus activos visuales (imágenes, etc.) y juzgar qué epsilon puede ser.
  2. No intente ningún cálculo de coma flotante hasta que haya leído la referencia de la respuesta sin culto a la carga (y haya obtenido su Ph.D en el proceso) y luego use su juicio no intuitivo para seleccionar K

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

La respuesta correcta: CGPointEqualToPoint ().

Una pregunta diferente: ¿dos valores calculados son los mismos?

La respuesta publicada aquí: No lo son.

¿Cómo comprobar si están cerca? Si desea verificar si están cerca, entonces no use CGPointEqualToPoint (). Pero no revises para ver 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.

La última vez que verifiqué el estándar C, no hubo ningún requisito para que las operaciones de punto flotante en dobles (64 bits en total, mantisa de 53 bits) tengan una precisión superior a esa precisión. Sin embargo, algún hardware podría hacer las operaciones en registros de mayor precisión, y el requisito se interpretó como que no requería borrar bits de orden inferior (más allá de la precisión de los números cargados en los registros). Por lo tanto, podría obtener resultados inesperados de comparaciones como esta dependiendo de lo que quedaba en los registros de quien durmió allí la última vez.

Dicho esto, ya pesar de mis esfuerzos por borrarlo cada vez que lo veo, el equipo donde trabajo tiene un montón de código C que se comstack con gcc y 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á limpiando los bits de orden baja para nosotros, los registros de 80 bits no se usan para estas operaciones en las computadoras modernas, el estándar se ha cambiado o qué. Me gustaría saber si alguien puede citar un capítulo y un versículo.

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

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

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

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í mismo. 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, barras redondas de metal de entre 1 y 2 pulgadas podrían considerarse de igual diámetro si sus diámetros difieren en menos de 0.0001 pulgadas. Entonces llamarías a setAcceptableDifference con el parámetro 0.0001, y luego usarás el operador de igualdad con confianza.