No, esta no es otra pregunta "¿Por qué es (1 / 3.0) * 3! = 1" .
He estado leyendo mucho sobre puntos flotantes últimamente; específicamente, cómo el mismo cálculo podría dar diferentes resultados en diferentes arquitecturas o configuraciones de optimización.
Este es un problema para los videojuegos que almacenan repeticiones o están conectados en red entre pares (a diferencia del 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 conducir a un estado de juego drásticamente diferente en diferentes máquinas (¡o incluso en la misma máquina! )
Esto sucede incluso entre los procesadores que "siguen" IEEE-754 , principalmente porque algunos procesadores (concretamente x86) utilizan una precisión doblemente extendida . Es decir, usan registros de 80 bits para hacer todos los cálculos, luego se truncan a 64 o 32 bits, lo que lleva a resultados de redondeo diferentes que las máquinas que usan 64 o 32 bits para los cálculos.
He visto varias soluciones a este problema en línea, pero todas para C ++, no C #:
- Deshabilite el modo de doble precisión extendida (para que todos los
double
cálculos usen IEEE-754 de 64 bits) usando_controlfp_s
(Windows),_FPU_SETCW
(Linux?) Ofpsetprec
(BSD). - Siempre ejecute el mismo compilador con la misma configuración de optimización y requiera 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 optimizar de manera diferente cada vez que se ejecuta el programa , no creo que esto sea posible.
- Utilice la aritmética de punto fijo, y evite
float
y pordouble
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 incluso un problema en C #? ¿Qué sucede si solo pretendo admitir Windows (no Mono)?
Si es así, ¿hay alguna forma de obligar a mi programa a ejecutarse con doble precisión normal?
Si no, ¿hay alguna biblioteca que ayude a mantener consistentes los cálculos de punto flotante?
strictfp
palabra clave, que obliga a todos los cálculos a realizarse en el tamaño indicado (float
odouble
) en lugar de un tamaño extendido. Sin embargo, Java todavía tiene muchos problemas con el soporte IEE-754. Muy (muy, muy) pocos lenguajes de programación admiten bien IEE-754.Respuestas:
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). Por lo tanto,
float
no es posible usar s normales en código determinista .net.Las soluciones que consideré:
Acabo de comenzar una implementación de software de matemática de coma flotante de 32 bits. Puede hacer alrededor de 70 millones de adiciones / multiplicaciones por segundo en mi i3 de 2.66 GHz. https://github.com/CodesInChaos/SoftFloat . Obviamente todavía está muy incompleto y con errores.
fuente
decimal
primero, ya que es mucho más simple de hacer. Solo si es demasiado lento para la tarea en cuestión, vale la pena pensar en otros enfoques.La especificación C # (§4.1.6 tipos de punto flotante) específicamente permite que los cálculos de punto flotante se realicen utilizando una precisión mayor que la del resultado. Entonces, no, no creo que pueda hacer que esos cálculos sean deterministas directamente en .Net. Otros sugirieron varias soluciones, por lo que podría probarlas.
fuente
double
cada vez después de una operación para quitar los bits no deseados y obtener resultados consistentes?La siguiente página puede ser útil en el caso de que necesite portabilidad absoluta de tales operaciones. Analiza el software para probar implementaciones del estándar IEEE 754, incluido el software para emular operaciones de punto flotante. Sin embargo, la mayoría de la información es probablemente específica de C o C ++.
http://www.math.utah.edu/~beebe/software/ieee/
Una nota sobre punto fijo
Los números binarios de punto fijo también pueden funcionar bien como un sustituto del punto flotante, como es evidente por las cuatro operaciones aritméticas básicas:
Los números de punto fijo binario se pueden implementar en cualquier tipo de datos enteros, como int, long y BigInteger, y los tipos no compatibles con CLS uint y ulong.
Como se sugiere en otra respuesta, puede usar tablas de búsqueda, donde cada elemento de la tabla es un número de punto fijo binario, para ayudar a implementar funciones complejas como seno, coseno, raíz cuadrada, etc. Si la tabla de búsqueda es menos granular que el número de punto fijo, se sugiere redondear la entrada agregando la mitad de la granularidad de la tabla de búsqueda a la entrada:
fuente
const
lugar destatic
constantes, para que el compilador pueda optimizarlos; preferir funciones miembro a funciones estáticas (para que podamos llamar, ej. enmyDouble.LeadingZeros()
lugar deIntDouble.LeadingZeros(myDouble)
); trate de evitar nombres de variables de una letra (MultiplyAnyLength
por ejemplo, tiene 9, lo que hace que sea muy difícil de seguir)unchecked
tipos no compatibles con CLS comoulong
,uint
etc., para fines de velocidad, ya que se usan muy raramente, el JIT no los optimiza tan agresivamente, por lo que usarlos puede ser más lento que usar tipos normales comolong
yint
. Además, C # tiene una sobrecarga del operador , de lo que este proyecto se beneficiaría enormemente. Finalmente, ¿hay pruebas unitarias asociadas? Además de esas pequeñas cosas, increíble trabajo Peter, ¡esto es ridículamente impresionante!strictfp
.¿Es esto un problema para C #?
Si. Diferentes arquitecturas son la menor de sus preocupaciones, diferentes velocidades de cuadros, etc. pueden conducir a desviaciones debido a imprecisiones en las representaciones flotantes, incluso si son las mismas inexactitudes (por ejemplo, la misma arquitectura, excepto una GPU más lenta en una máquina).
¿Puedo usar System.Decimal?
No hay razón para que no puedas hacerlo, sin embargo, es un perro lento.
¿Hay alguna manera de obligar a mi programa a ejecutarse con doble precisión?
Si. Hospede el tiempo de ejecución CLR usted mismo ; y compila todas las llamadas / banderas de nessecary (que cambian el comportamiento de la aritmética de coma flotante) en la aplicación C ++ antes de llamar a CorBindToRuntimeEx.
¿Hay alguna biblioteca que ayude a mantener consistentes los cálculos de coma flotante?
No que yo sepa.
¿Hay otra forma de resolver esto?
Ya he abordado este problema antes, la idea es usar QNumbers . Son una forma de reales que son de punto fijo; pero no punto fijo en base-10 (decimal) - más bien base-2 (binario); debido a esto, las primitivas matemáticas en ellas (suma, sub, mul, div) son mucho más rápidas que los ingenuos puntos fijos de base 10; especialmente si
n
es lo mismo para ambos valores (que en su caso lo sería). Además, debido a que son integrales, tienen resultados bien definidos en cada plataforma.Tenga en cuenta que la velocidad de fotogramas todavía puede afectarlos, pero no es tan malo y se puede rectificar fácilmente utilizando puntos de sincronización.
¿Puedo usar más funciones matemáticas con QNumbers?
Sí, redondea un decimal para hacer esto. Además, realmente debería usar tablas de búsqueda para las funciones trigonométricas (sin, cos); ya que realmente pueden dar diferentes resultados en diferentes plataformas, y si los codifica correctamente, pueden usar QNumbers directamente.
fuente
De acuerdo con esta entrada de blog de MSDN un poco antigua, el JIT no usará SSE / SSE2 para coma flotante, todo es x87. Por eso, como mencionó, debe preocuparse por los modos y las banderas, y en C # eso no es posible controlarlo. Por lo tanto, el uso de operaciones normales de coma flotante no garantizará exactamente el mismo resultado en cada máquina para su programa.
Para obtener una reproducibilidad precisa de doble precisión, tendrá que hacer una emulación de punto flotante (o punto fijo) de software. No sé de las bibliotecas de C # para hacer esto.
Dependiendo de las operaciones que necesite, es posible que pueda escapar con una precisión única. Aquí está la idea:
El gran problema con x87 es que los cálculos pueden hacerse con una precisión de 53 bits o 64 bits, dependiendo del indicador de precisión y de si el registro se derramó en la memoria. Pero para muchas operaciones, realizar la operación con alta precisión y redondear a una precisión menor garantizará la respuesta correcta, lo que implica que se garantizará que la respuesta sea la misma en todos los sistemas. Si obtiene la precisión adicional no importará, ya que tiene 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 generalmente coincidirán pero no hay garantía). "¿Cuándo es inocuo el doble redondeo?" Referencia de ACM (req. De pago)
¡Espero que esto ayude!
fuente
Como ya se dijo en otras respuestas: Sí, este es un problema en C #, incluso cuando se mantiene puro Windows.
En cuanto a una solución: puede reducir (y con un poco de esfuerzo / rendimiento) evitar el problema por completo si usa la
BigInteger
clase integrada y escala todos los cálculos a una precisión definida mediante el uso de 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 un 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 en esta estructura de datos y no puede usar ninguna instrucción de coma flotante integrada en la CPU.La
BigInteger
"solución" hace algo similar: solo que usted puede definir cuántos dígitos necesita / desea ... quizás solo desee 80 bits o 240 bits de precisión.La lentitud proviene siempre de tener que simular todas las operaciones en este número a través de instrucciones de solo entero sin usar las instrucciones incorporadas 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 (ver respuesta de Jonathan Dickinson: ¿las matemáticas de punto flotante son consistentes en C #? ¿Puede ser? ) Y / o el almacenamiento en caché (por ejemplo, cálculos trigonométricos ...) etc.
fuente
BigInteger
solo está disponible en .Net 4.0.BigInteger
supera incluso el rendimiento alcanzado por Decimal.Decimal
(@Jonathan Dickinson - 'dog slow') oBigInteger
(@CodeInChaos comentario arriba) - ¿alguien puede dar una pequeña explicación sobre estos éxitos en el desempeño y si / por qué realmente son un obstáculo para proporcionar una solución.Bueno, aquí estaría mi primer intento de cómo hacer esto :
(Creo que puede compilar a un archivo .dll de 32 bits y luego usarlo con x86 o AnyCpu [o probablemente solo apunte a x86 en un sistema de 64 bits; vea el comentario a continuación]).
Luego, suponiendo que funcione, si desea usar Mono, imagino que debería poder replicar la biblioteca en otras plataformas x86 de manera similar (no COM, por supuesto; aunque, tal vez, ¿con vino? Un poco fuera de mi área una vez vamos allí sin embargo ...).
Suponiendo que puede hacer que funcione, debería poder configurar funciones personalizadas que pueden realizar múltiples operaciones a la vez para solucionar cualquier problema de rendimiento, y tendrá matemática de punto flotante que le permite tener resultados consistentes en todas las plataformas con una cantidad mínima de código escrito en C ++, y dejando el resto de su código en C #.
fuente
x86
puede cargar el dll de 32 bits.No soy un desarrollador de juegos, aunque tengo mucha experiencia con problemas computacionalmente difíciles ... así que haré lo mejor que pueda.
La estrategia que adoptaría es esencialmente esta:
El resumen de esto es: necesitas encontrar un equilibrio. Si está gastando 30 ms de renderizado (~ 33 fps) y solo 1 ms haciendo detección de colisión (o inserta alguna otra operación altamente sensible), incluso si triplica el tiempo que lleva hacer la aritmética crítica, el impacto que tiene en su velocidad de cuadros es baja de 33.3 fps a 30.3 fps.
Le sugiero que haga un perfil de todo, tenga en cuenta la cantidad de tiempo que pasa haciendo cada uno de los cálculos notablemente caros, luego repita las mediciones con 1 o más métodos para resolver este problema y vea cuál es el impacto.
fuente
Verificando los enlaces en las otras respuestas deja en claro que nunca tendrá la garantía de si el punto flotante se implementa "correctamente" o si siempre recibirá una cierta precisión para un cálculo dado, pero quizás podría hacer un mejor esfuerzo al (1) truncando todos los cálculos a un mínimo común (por ejemplo, si diferentes implementaciones le darán 32 a 80 bits de precisión, siempre truncando cada operación a 30 o 31 bits), (2) tenga una tabla de algunos casos de prueba al inicio (casos límite de sumar, restar, multiplicar, dividir, sqrt, coseno, etc.) y si la implementación calcula valores que coinciden con la tabla, no se moleste en hacer ningún ajuste.
fuente
float
tipo de datos en máquinas x86, sin embargo, esto causará resultados ligeramente diferentes de las máquinas que hacen todos sus cálculos usando solo 32 bits, y estos pequeños cambios se propagarán con el tiempo. De ahí la pregunta.Su pregunta en cosas bastante difíciles y técnicas O_o. Sin embargo, puedo tener una idea.
Seguro que sabe que la CPU realiza algunos ajustes después de cualquier operación flotante. Y la CPU ofrece varias instrucciones diferentes que hacen diferentes operaciones de redondeo.
Entonces, para una expresión, su compilador elegirá un conjunto de instrucciones que lo llevarán a un resultado. Pero cualquier otro flujo de trabajo de instrucciones, incluso si tienen la intención de calcular la misma expresión, puede proporcionar otro resultado.
Los 'errores' cometidos por un ajuste de redondeo crecerán con cada instrucción adicional.
Como ejemplo, podemos decir que a nivel de ensamblaje: a * b * c no es equivalente a a * c * b.
No estoy completamente seguro de eso, tendrá que preguntar por alguien que conozca la arquitectura de la CPU mucho más que yo: p
Sin embargo, para responder a su pregunta: en C o C ++ puede resolver su problema porque tiene cierto control sobre el código de máquina generado por su compilador, sin embargo, en .NET no tiene ninguno. Por lo tanto, siempre que su código de máquina pueda ser diferente, nunca estará seguro del resultado exacto.
Tengo curiosidad por saber de qué manera esto puede ser un problema porque la variación parece muy mínima, pero si necesita una operación realmente precisa, la única solución en la que puedo pensar será aumentar el tamaño de sus registros flotantes. Use doble precisión o incluso doble largo si puede (no estoy seguro de que sea posible usando CLI).
Espero haber sido lo suficientemente claro, no soy perfecto en inglés (... en absoluto: s)
fuente