¿Las matemáticas de punto flotante son consistentes en C#? ¿Puede ser?
No, esta no es otra pregunta de "¿Por qué es (1/3.0)*3! = 1" .
Últimamente he estado leyendo mucho sobre puntos flotantes; específicamente, cómo el mismo cálculo puede dar resultados diferentes en diferentes arquitecturas o configuraciones de optimización.
Este es un problema para los videojuegos que almacenan repeticiones o que están conectados en red de igual a igual (a diferencia de servidor-cliente), que dependen de que todos los clientes generen exactamente los mismos resultados cada vez que ejecutan el programa: una pequeña discrepancia en uno. El cálculo de punto flotante puede llevar a un estado de juego drásticamente diferente en diferentes máquinas (¡o incluso en la misma máquina! ) .
Esto sucede incluso entre procesadores que "siguen" IEEE-754 , principalmente porque algunos procesadores (concretamente x86) utilizan doble precisión extendida . Es decir, utilizan registros de 80 bits para realizar todos los cálculos y luego los truncan a 64 o 32 bits, lo que genera resultados de redondeo diferentes a los de las máquinas que utilizan 64 o 32 bits para los cálculos.
He visto varias soluciones a este problema en línea, pero todas para C++, no para C#:
- Deshabilite el modo de doble precisión extendida (para que todos
double
los cálculos utilicen IEEE-754 de 64 bits) usando_controlfp_s
(Windows),_FPU_SETCW
(¿Linux?) ofpsetprec
(BSD). - Ejecute siempre el mismo compilador con la misma configuración de optimización y exija que todos los usuarios tengan la misma arquitectura de CPU (sin juego multiplataforma). Debido a que mi "compilador" es en realidad el JIT, que puede optimizarse de manera diferente cada vez que se ejecuta el programa , no creo que esto sea posible.
- Utilice aritmética de punto fijo y evite
float
ydouble
por completo.decimal
funcionaría para este propósito, pero sería mucho más lento y ninguna de lasSystem.Math
funciones de la biblioteca lo admite.
Entonces, ¿es esto siquiera un problema en C#? ¿Qué pasa si sólo pretendo admitir Windows (no Mono)?
Si es así, ¿hay alguna forma de forzar que mi programa se ejecute con doble precisión normal?
Si no es así, ¿existen bibliotecas que ayuden a mantener la coherencia de los cálculos de punto flotante?
No conozco ninguna manera de hacer que los puntos flotantes normales sean deterministas en .net. El JITter puede crear código que se comporta de manera diferente en diferentes plataformas (o entre diferentes versiones de .net). float
Por lo tanto , no es posible utilizar s normales en código .net determinista.
Las soluciones que consideré:
- Implementar FixPoint32 en C#. Si bien esto no es demasiado difícil (tengo una implementación a medio terminar), el rango muy pequeño de valores hace que su uso sea molesto. Hay que tener cuidado en todo momento para no desbordar, ni perder demasiada precisión. Al final, encontré que esto no es más fácil que usar números enteros directamente.
- Implementar FixPoint64 en C#. Esto me resultó bastante difícil de hacer. Para algunas operaciones, serían útiles números enteros intermedios de 128 bits. Pero .net no ofrece ese tipo.
- Implemente un punto flotante personalizado de 32 bits. La falta de un BitScanReverse intrínseco causa algunas molestias al implementarlo. Pero actualmente creo que este es el camino más prometedor.
- Utilice código nativo para las operaciones matemáticas. Incurre en la sobrecarga de una llamada de delegado en cada operación matemática.
Acabo de comenzar una implementación de software de matemáticas de punto flotante de 32 bits. Puede realizar alrededor de 70 millones de sumas/multiplicaciones por segundo en mi i3 de 2,66 GHz. https://github.com/CodesInChaos/SoftFloat . Obviamente todavía está muy incompleto y con errores.
La especificación C# (§4.1.6 Tipos de coma flotante) permite específicamente realizar cálculos de coma flotante utilizando una precisión superior a la del resultado. Entonces, no, no creo que puedas hacer que esos cálculos sean deterministas directamente en .Net. Otros sugirieron varias soluciones, por lo que podrías probarlas.
¿Es esto un problema para C#?
Sí. Las diferentes arquitecturas son la menor de sus preocupaciones, diferentes velocidades de fotogramas, etc., pueden provocar desviaciones debido a imprecisiones en las representaciones flotantes, incluso si son las mismas imprecisiones (por ejemplo, la misma arquitectura, excepto una GPU más lenta en una máquina).
¿Puedo usar System.Decimal?
No hay ninguna razón por la que no puedas, sin embargo, es muy lento.
¿Hay alguna manera de forzar que mi programa se ejecute con doble precisión?
Sí. Aloje usted mismo el tiempo de ejecución de CLR ; y compilar todas las llamadas/indicadores necesarios (que cambian el comportamiento de la aritmética de punto flotante) en la aplicación C++ antes de llamar a CorBindToRuntimeEx.
¿Existe alguna biblioteca que ayude a mantener consistentes los cálculos de punto flotante?
No que yo sepa.
¿Hay otra manera de solucionar esto?
He abordado este problema antes, la idea es usar QNumbers . Son una forma de reales de punto fijo; pero no punto fijo en base 10 (decimal), sino en base 2 (binario); debido a esto, las primitivas matemáticas que contienen (suma, sub, mul, div) son mucho más rápidas que los ingenuos puntos fijos de base 10; especialmente si n
es el mismo para ambos valores (que en su caso sería). Además al ser integrales tienen resultados bien definidos en cada plataforma.
Tenga en cuenta que la velocidad de fotogramas aún puede afectarlos, pero no es tan grave y se rectifica fácilmente mediante puntos de sincronización.
¿Puedo usar más funciones matemáticas con QNumbers?
Sí, haga un viaje de ida y vuelta con un decimal para hacer esto. Además, debería utilizar tablas de búsqueda para las funciones trigonométricas (sin, cos); ya que realmente pueden dar resultados diferentes en diferentes plataformas, y si los codifica correctamente, pueden usar QNumbers directamente.
Según esta entrada de blog de MSDN ligeramente antigua , JIT no utilizará SSE/SSE2 para punto flotante, es todo x87. Por eso, como mencionaste, debes preocuparte por los modos y las banderas, y en C# eso no es posible de controlar. Por lo tanto, el uso de operaciones normales de punto flotante no garantizará exactamente el mismo resultado en todas las máquinas para su programa.
Para obtener una reproducibilidad precisa de doble precisión, tendrá que realizar una emulación de punto flotante (o punto fijo) por software. No conozco bibliotecas de C# para hacer esto.
Dependiendo de las operaciones que necesite, es posible que pueda salirse con la suya con precisión simple. Aquí está la idea:
- almacene todos los valores que le interesan con precisión simple
- para realizar una operación:
- ampliar las entradas a doble precisión
- realizar operaciones con doble precisión
- convertir el resultado a precisión simple
El gran problema con x87 es que los cálculos se pueden realizar con una precisión de 53 o 64 bits dependiendo del indicador de precisión y de si el registro se desbordó en la memoria. Pero para muchas operaciones, realizar la operación con alta precisión y redondear a una precisión más baja garantizará la respuesta correcta, lo que implica que se garantizará que la respuesta será la misma en todos los sistemas. No importará si obtienes la precisión adicional, ya que tienes suficiente precisión para garantizar la respuesta correcta en cualquier caso.
Operaciones que deberían funcionar en este esquema: suma, resta, multiplicación, división, sqrt. Cosas como sin, exp, etc. no funcionarán (los resultados normalmente coincidirán pero no hay garantía). "¿Cuándo es inocuo el doble redondeo?" Referencia de ACM (requisito de registro pagado)
¡Espero que esto ayude!
Como ya se indicó en otras respuestas: Sí, esto es un problema en C#, incluso cuando se utiliza Windows puro.
En cuanto a una solución: puede reducir (y con algo de esfuerzo/rendimiento) evitar el problema por completo si usa la clase incorporada BigInteger
y escala todos los cálculos a una precisión definida usando un denominador común para cualquier cálculo/almacenamiento de dichos números.
Según lo solicitado por OP - con respecto al rendimiento:
System.Decimal
representa un número con 1 bit para signo y un entero de 96 bits y una "escala" (que representa dónde está el punto decimal). Para todos los cálculos que realice, debe operar con esta estructura de datos y no puede utilizar ninguna instrucción de punto flotante integrada en la CPU.
La BigInteger
"solución" hace algo similar, sólo que puede definir cuántos dígitos necesita/quiere... tal vez desee sólo 80 bits o 240 bits de precisión.
La lentitud siempre proviene de tener que simular todas las operaciones en estos números mediante instrucciones de solo números enteros sin usar las instrucciones integradas de CPU/FPU, lo que a su vez conduce a muchas más instrucciones por operación matemática.
Para reducir el impacto en el rendimiento, existen varias estrategias, como QNumbers (consulte la respuesta de Jonathan Dickinson: ¿Las matemáticas de punto flotante son consistentes en C#? ¿Puede serlo? ) y/o el almacenamiento en caché (por ejemplo, cálculos trigonométricos...), etc.