¿Qué tan determinista es la inexactitud del punto flotante?

Entiendo que los cálculos de coma flotante tienen problemas de precisión y hay muchas preguntas que explican por qué. Mi pregunta es si ejecuto el mismo cálculo dos veces, ¿siempre puedo confiar en que produzca el mismo resultado? ¿Qué factores pueden afectar esto?

  • Tiempo entre cálculos?
  • ¿Estado actual de la CPU?
  • Hardware diferente?
  • Idioma / plataforma / sistema operativo?
  • ¿Erupciones solares?

Tengo una simulación de física simple y me gustaría grabar sesiones para que puedan reproducirse. Si se puede confiar en los cálculos, solo debería registrar el estado inicial más cualquier entrada del usuario y siempre debería ser capaz de reproducir exactamente el estado final. Si los cálculos no son precisos, los errores al inicio pueden tener enormes implicaciones al final de la simulación.

Actualmente estoy trabajando en Silverlight, aunque estaría interesado en saber si esta pregunta puede ser respondida en general.

Actualización: las respuestas iniciales indican que sí, pero aparentemente esto no es del todo claro, tal como se discutió en los comentarios para la respuesta seleccionada. Parece que tendré que hacer algunas pruebas y ver qué pasa.

Por lo que entiendo, solo tiene garantizados resultados idénticos siempre que esté tratando con el mismo conjunto de instrucciones y el mismo comstackdor, y que los procesadores en los que se ejecuta se adhieren estrictamente a los estándares relevantes (es decir, IEEE754). Dicho esto, a menos que se trate de un sistema particularmente caótico, cualquier deriva en el cálculo entre ejecuciones probablemente no genere un comportamiento problemático.

Problemas específicos que conozco:

1.) algunos sistemas operativos le permiten establecer el modo del procesador de punto flotante de manera que rompa la compatibilidad.

2.) los resultados intermedios de coma flotante a menudo usan precisión de 80 bit en el registro, pero solo 64 bit en la memoria. Si un progtwig se recomstack de una manera que cambia el derrame de registros dentro de una función, puede devolver resultados diferentes en comparación con otras versiones. La mayoría de las plataformas le darán la posibilidad de forzar que todos los resultados se trunquen con la precisión de la memoria.

3.) las funciones de biblioteca estándar pueden cambiar entre versiones. Deduzco que hay algunos ejemplos no infrecuentes de esto en gcc 3 vs 4.

4.) El IEEE mismo permite que algunas representaciones binarias difieran … específicamente valores de NaN, pero no recuerdo los detalles.

La respuesta corta es que los cálculos de FP son completamente deterministas, según el estándar IEEE de punto flotante , pero eso no significa que sean totalmente reproducibles en máquinas, comstackdores, sistemas operativos, etc.

La respuesta larga a estas preguntas y más se puede encontrar en lo que es probablemente la mejor referencia sobre coma flotante, David Goldberg, ” Lo que cada científico debería saber sobre la aritmética de punto flotante” . Vaya a la sección sobre el estándar IEEE para los detalles clave.

Para responder a sus viñetas brevemente:

  • El tiempo entre los cálculos y el estado de la CPU tienen poco que ver con esto.

  • El hardware puede afectar las cosas (por ejemplo, algunas GPU no son compatibles con el punto flotante IEEE).

  • El lenguaje, la plataforma y el sistema operativo también pueden afectar las cosas. Para una mejor descripción de esto que yo puedo ofrecer, vea la respuesta de Jason Watkins. Si está utilizando Java, eche un vistazo al diatriba de Kahan sobre las insuficiencias de coma flotante de Java .

  • Las erupciones solares podrían importar, con suerte con poca frecuencia. No me preocuparía demasiado, porque si importan, todo lo demás también está mal. Pondría esto en la misma categoría que preocuparme por EMP .

Finalmente, si está haciendo la misma secuencia de cálculos de coma flotante en las mismas entradas iniciales, entonces las cosas deberían volver a ser exactamente iguales. La secuencia exacta puede cambiar dependiendo de su comstackdor / os / biblioteca estándar, por lo que puede obtener algunos pequeños errores de esta manera.

Donde normalmente se encuentra con problemas en coma flotante es si tiene un método numéricamente inestable y comienza con entradas FP que son aproximadamente las mismas pero no del todo. Si su método es estable, debe poder garantizar la reproducibilidad dentro de cierta tolerancia. Si desea más detalles, revise el artículo FP de Goldberg vinculado anteriormente o elija un texto introductorio sobre análisis numérico.

Creo que su confusión radica en el tipo de inexactitud alrededor del punto flotante. La mayoría de los lenguajes implementan el estándar de coma flotante IEEE. Esta norma establece cómo se usan los bits individuales dentro de un flotante / doble para producir un número. Normalmente, un flotante consta de cuatro bytes y un doble de ocho bytes.

Una operación matemática entre dos números de coma flotante tendrá el mismo valor cada vez (como se especifica dentro del estándar).

La imprecisión viene en la precisión. Considere un int frente a un flotador. Ambos suelen ocupar el mismo número de bytes (4). Sin embargo, el valor máximo que cada número puede almacenar es tremendamente diferente.

  • int: aproximadamente 2,000 millones
  • flotador: 3.40282347E38 (bastante más grande)

La diferencia está en el medio. int, puede representar cada número entre 0 y aproximadamente 2 mil millones. Float sin embargo no puede. Puede representar 2 mil millones de valores entre 0 y 3.40282347E38. Pero eso deja una gama completa de valores que no se pueden representar. Si una ecuación matemática alcanza uno de estos valores, deberá redondearse a un valor representable y, por lo tanto, se considera “inexacta”. Su definición de inexacta puede variar :).

Además, aunque Goldberg es una gran referencia, el texto original también es incorrecto: IEEE754 no está seguro de ser portátil . No puedo enfatizar esto lo suficiente, dada la frecuencia con la que se hace esta afirmación en función de rozar el texto. Las versiones posteriores del documento incluyen una sección que analiza esto específicamente :

Muchos progtwigdores pueden no darse cuenta de que incluso un progtwig que usa solo los formatos numéricos y las operaciones prescritas por el estándar IEEE puede calcular diferentes resultados en diferentes sistemas. De hecho, los autores del estándar intentaron permitir diferentes implementaciones para obtener resultados diferentes.

Lo siento, pero no puedo evitar pensar que a todos les falta el punto.

Si la inexactitud es significativa para lo que está haciendo, entonces debe buscar un algoritmo diferente.

Usted dice que si los cálculos no son precisos, los errores al inicio pueden tener enormes implicaciones al final de la simulación.

Que mi amigo no es una simulación Si obtiene resultados muy diferentes debido a las pequeñas diferencias debidas al redondeo y la precisión, es probable que ninguno de los resultados tenga validez. El hecho de que pueda repetir el resultado no lo hace más válido.

En cualquier problema no trivial del mundo real que incluya mediciones o cálculos no enteros, siempre es una buena idea introducir errores menores para probar qué tan estable es su algoritmo.

Esta respuesta en C ++ FAQ probablemente la describa mejor:

http://www.parashift.com/c++-faq-lite/newbie.html#faq-29.18

No es solo que diferentes architectures o comstackdores puedan causarle problemas, los números que apuntan al flotador ya se comportan de forma extraña dentro del mismo progtwig. Como las preguntas frecuentes señalan si y == x es verdadero, eso todavía puede significar que cos(y) == cos(x) será falso. Esto se debe a que la CPU x86 calcula el valor con 80 bits, mientras que el valor se almacena como 64 bits en la memoria, por lo que termina comparando un valor truncado de 64 bits con un valor completo de 80 bits.

El cálculo sigue siendo determinista, en el sentido de que ejecutar el mismo binario comstackdo le dará el mismo resultado cada vez, pero en el momento en que ajusta un poco la fuente, la optimización indica o comstack con un comstackdor diferente, todas las apuestas están desactivadas y cualquier cosa puede pasar.

Hablando en términos prácticos, no es tan malo, podría reproducir cálculos matemáticos simples con diferentes versiones de GCC en bits de Linux de 32 bits, pero en el momento en que cambié a Linux de 64 bits el resultado ya no era el mismo. Las grabaciones de Demos creadas en 32 bits no funcionarían en 64 bits y viceversa, pero funcionarían bien cuando se ejecuten en el mismo arco.

Como su pregunta está etiquetada C #, vale la pena enfatizar los problemas que enfrenta .NET:

  1. Las matemáticas de coma flotante no son asociativas, es decir, (a + b) + c no se garantiza que sean igual a + (b + c) ;
  2. Diferentes comstackdores optimizarán tu código de diferentes maneras, y eso puede implicar reordenamiento de operaciones aritméticas.
  3. En .NET, el comstackdor JIT de CLR comstackrá su código sobre la marcha, por lo que la comstackción depende de la versión de .NET en la máquina en tiempo de ejecución.

Esto significa que no debe confiar en que su aplicación .NET produzca los mismos resultados de cálculo de punto flotante cuando se ejecute en diferentes versiones de .NET CLR.

Por ejemplo, en su caso, si graba el estado inicial y las entradas en su simulación, entonces instale un paquete de servicio que actualice el CLR, es posible que su simulación no se reproduzca de manera idéntica la próxima vez que la ejecute.

Ver la publicación de Shawn Hargreaves en el blog. ¿El punto flotante es matemático determinista? para una discusión adicional relevante para .NET.

HM. Desde que OP solicitó C #:

¿El determinante JIT del bytecode de C # es determinista o genera un código diferente entre diferentes ejecuciones? No lo sé, pero no confiaría en el Jit.

Podría pensar en escenarios donde el JIT tiene algunas características de calidad de servicio y decide dedicar menos tiempo a la optimización porque la CPU está haciendo un gran número de crujidos en otro lugar (¿piensa en la encoding de DVD de fondo)? Esto podría conducir a diferencias sutiles que pueden dar lugar a grandes diferencias más adelante.

Además, si el JIT se mejora (tal vez como parte de un paquete de servicio), el código generado cambiará con seguridad. El problema de precisión interna de 80 bits ya ha sido mencionado.

Esta no es una respuesta completa a su pregunta, pero aquí hay un ejemplo que demuestra que los cálculos dobles en C # no son deterministas. No sé por qué, pero el código aparentemente no relacionado aparentemente puede afectar el resultado de un doble cálculo descendente.

  1. Cree una nueva aplicación WPF en Visual Studio Versión 12.0.40629.00 Actualización 5 y acepte todas las opciones predeterminadas.
  2. Reemplace el contenido de MainWindow.xaml.cs con esto:

     using System; using System.Windows; namespace WpfApplication1 { ///  /// Interaction logic for MainWindow.xaml ///  public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Content = FooConverter.Convert(new Point(950, 500), new Point(850, 500)); } } public static class FooConverter { public static string Convert(Point curIPJos, Point oppIJPos) { var ij = " Insulated Joint"; var deltaX = oppIJPos.X - curIPJos.X; var deltaY = oppIJPos.Y - curIPJos.Y; var teta = Math.Atan2(deltaY, deltaX); string result; if (-Math.PI / 4 < = teta && teta <= Math.PI / 4) result = "Left" + ij; else if (Math.PI / 4 < teta && teta <= Math.PI * 3 / 4) result = "Top" + ij; else if (Math.PI * 3 / 4 < teta && teta <= Math.PI || -Math.PI <= teta && teta <= -Math.PI * 3 / 4) result = "Right" + ij; else result = "Bottom" + ij; return result; } } } 
  3. Establezca la configuración de comstackción en "Versión" y comstackción, pero no la ejecute en Visual Studio.

  4. Haga doble clic en el exe creado para ejecutarlo.
  5. Tenga en cuenta que la ventana muestra "Junta aislada inferior".
  6. Ahora agregue esta línea justo antes de "resultado de cadena":

     string debug = teta.ToString(); 
  7. Repita los pasos 3 y 4.

  8. Tenga en cuenta que la ventana muestra "Junta aislada derecha".

Este comportamiento fue confirmado en la máquina de un colega. Tenga en cuenta que la ventana muestra consistentemente "Unión aislada derecha" si cualquiera de los siguientes es verdadero: el exe se ejecuta desde Visual Studio, el exe se construyó usando la configuración de depuración, o "Preferir 32 bits" no está marcado en las propiedades del proyecto.

Es bastante difícil averiguar qué está pasando, ya que cualquier bash de observar el proceso parece cambiar el resultado.

Muy pocas FPU cumplen con el estándar IEEE (a pesar de sus afirmaciones). Por lo tanto, ejecutar el mismo progtwig en diferentes hardware le dará resultados diferentes. Es probable que los resultados sean en casos de esquina que ya debería evitar como parte del uso de una FPU en su software.

Los errores de IEEE suelen estar parcheados en el software y ¿está seguro de que el sistema operativo que está ejecutando hoy incluye las trampas y parches adecuados del fabricante? ¿Qué pasa antes o después de que el sistema operativo tenga una actualización? ¿Se eliminan todos los errores y se agregan correcciones de errores? ¿El comstackdor de C está sincronizado con todo esto y el comstackdor de C produce el código correcto?

Probando esto puede ser inútil. No verá el problema hasta que entregue el producto.

Observe la regla FP número 1: Nunca use una comparación if (algo == algo). Y la regla número dos IMO tendría que ver con ascii a fp o fp a ascii (printf, scanf, etc.). Hay más problemas de precisión y errores allí que en el hardware.

Con cada nueva generación de hardware (densidad), los efectos del sol son más evidentes. Ya tenemos problemas con los SEU en la superficie de los planetas, por lo que, independientemente de los cálculos de coma flotante, tendrá problemas (algunos proveedores se han molestado en preocuparse, por lo que se esperan choques más a menudo con el nuevo hardware).

Al consumir enormes cantidades de lógica, es probable que el fpu sea muy rápido (un solo ciclo de reloj). No es más lento que un entero alu. No confundas esto con que los fpus modernos son tan simples como alus, los fpus son caros. (Al igual que consumen más lógica para multiplicar y dividir para obtener eso a un ciclo de reloj, pero no es tan grande como el fpu).

Siga las reglas simples anteriores, estudie el punto flotante un poco más, comprenda las verrugas y las trampas que lo acompañan. Es posible que desee comprobar el infinito o nans periódicamente. Es más probable que sus problemas se encuentren en el comstackdor y el sistema operativo que en el hardware (en general, no solo en fp math). El hardware moderno (y el software) en la actualidad, por definición, está lleno de errores, así que intente tener menos errores de los que corre su software.