Sobre el manejo de números de coma flotante de manera determinista
El punto flotante es determinista. Bueno, debería ser. Es complicado.
Hay mucha literatura sobre números de coma flotante:
Y cómo son problemáticos:
Por resumen. Al menos, en un solo hilo, las mismas operaciones, con los mismos datos, que suceden en el mismo orden, deben ser deterministas. Por lo tanto, podemos comenzar preocupándonos por las entradas y reordenando.
Una de esas entradas que causa problemas es el tiempo.
En primer lugar, siempre debe calcular el mismo paso de tiempo. No digo que no midas el tiempo, digo que no pasarás el tiempo en la simulación física, porque las variaciones en el tiempo son una fuente de ruido en la simulación.
¿Por qué mide el tiempo si no lo pasa a la simulación de física? Desea medir el tiempo transcurrido para saber cuándo se debe llamar a un paso de simulación y, suponiendo que esté usando el modo de reposo, cuánto tiempo duerme.
Así:
- Medir el tiempo: Sí
- Usar tiempo en simulación: No
Ahora, reordenamiento de instrucciones.
El compilador podría decidir que f * a + b
es lo mismo b + f * a
, sin embargo, eso puede tener un resultado diferente. También podría compilarse en fmadd , o podría decidir tomar varias líneas como esa que suceden juntas y escribirlas con SIMD , o alguna otra optimización que no se me ocurra en este momento. Y recuerde que queremos que las mismas operaciones sucedan en el mismo orden, por lo que queremos controlar qué operaciones suceden.
Y no, usar el doble no te salvará.
Debe preocuparse por el compilador y su configuración, en particular para sincronizar números de coma flotante en la red. Debe obtener las compilaciones para aceptar hacer lo mismo.
Podría decirse que escribir ensamblaje sería ideal. De esa manera usted decide qué operación hacer. Sin embargo, eso podría ser un problema para soportar múltiples plataformas.
Así:
El caso de los números de punto fijo
Debido a la forma en que los flotadores se representan en la memoria, los valores grandes perderán precisión. Es lógico que mantener sus valores pequeños (abrazadera) mitiga el problema. Por lo tanto, no hay grandes velocidades ni habitaciones grandes. Lo que también significa que puede usar física discreta porque tiene menos riesgo de hacer túneles.
Por otro lado, se acumularán pequeños errores. Entonces, truncar. Quiero decir, escalar y emitir a un tipo entero. De esa manera, sabes que nada se está acumulando. Habrá operaciones que puede realizar manteniéndose con el tipo entero. Cuando necesitas volver al punto flotante, lanzas y deshaces la escala.
Tenga en cuenta que digo escala. La idea es que 1 unidad se represente realmente como una potencia de dos (16384 por ejemplo). Sea lo que sea, conviértalo en una constante y úselo. Básicamente lo está utilizando como número de punto fijo. De hecho, si puede usar números de punto fijo adecuados de alguna biblioteca confiable mucho mejor.
Estoy diciendo truncado. Sobre el problema de redondeo, significa que no puedes confiar en el último bit del valor que obtuviste después del lanzamiento. Entonces, antes de la escala de lanzamiento para obtener un poco más de lo que necesita, y truncarlo después.
Así:
- Mantenga valores pequeños: sí
- Redondeo cuidadoso: sí
- Números de puntos fijos cuando sea posible: sí
Espera, ¿por qué necesitas coma flotante? ¿No podrías trabajar solo con un tipo entero? Correcto. Trigonometría y radiación. Puede calcular tablas para trigonometría y radiación y hacer que se horneen en su fuente. O puede implementar los algoritmos utilizados para calcularlos con un número de coma flotante, excepto que utilice números de punto fijo. Sí, necesita equilibrar la memoria, el rendimiento y la precisión. Sin embargo, puede permanecer fuera de los números de coma flotante y ser determinista.
¿Sabías que hicieron cosas así para la PlayStation original? Por favor, conoce a mi perro, parches .
Por cierto, no estoy diciendo que no use punto flotante para gráficos. Solo por la física. Quiero decir, claro, las posiciones dependerán de la física. Sin embargo, como sabe, un colisionador no tiene que coincidir con un modelo. No queremos ver los resultados del truncamiento de los modelos.
Por lo tanto: USE NÚMEROS DE PUNTO FIJO.
Para ser claros, si puede usar un compilador que le permite especificar cómo funcionan los puntos flotantes, y eso es suficiente para usted, entonces puede hacerlo. Esa no siempre es una opción. Además, estamos haciendo esto para el determinismo. Los números de puntos fijos no significan que no haya errores, después de todo, tienen una precisión limitada.
No creo que "el número de punto fijo sea difícil" sea una buena razón para no usarlos. Y si desea una buena razón para usarlos, es el determinismo, en particular el determinismo en todas las plataformas.
Ver también:
Anexo : Sugiero mantener el tamaño del mundo pequeño. Dicho esto, tanto OP como Jibb Smart mencionan que alejarse de los flotadores de origen tienen menos precisión. Eso tendrá un efecto en la física, uno que se verá mucho antes que en el borde del mundo. Los números de puntos fijos, bueno, tienen una precisión fija, serán igualmente buenos (o malos, si lo prefiere) en todas partes. Lo cual es bueno si queremos determinismo. También quiero mencionar que la forma en que solemos hacer física tiene la propiedad de amplificar pequeñas variaciones. Vea The Butterfly Effect - Deterministic Physics en The Incredible Machine and Contraption Maker .
Otra forma de hacer física.
He estado pensando, la razón por la cual el pequeño error de precisión en los números de coma flotante se amplifica es porque estamos haciendo iteraciones en esos números. Cada paso de simulación tomamos los resultados del último paso de simulación y hacemos cosas sobre ellos. Acumulación de errores encima de errores. Ese es tu efecto mariposa.
No creo que veamos una sola compilación utilizando un solo subproceso en la misma máquina que produzca diferentes resultados con la misma entrada. Sin embargo, en otra máquina podría, o una construcción diferente podría.
Hay un argumento para probar allí. Si decidimos exactamente cómo deberían funcionar las cosas y podemos probar en el hardware de destino, no deberíamos publicar compilaciones que tengan un comportamiento diferente.
Sin embargo, también hay un argumento para no trabajar lejos que acumula tantos errores. Quizás esta sea una oportunidad para hacer física de una manera diferente.
Como sabrán, existe una física continua y discreta, ambas trabajan en cuánto avanzaría cada objeto en el paso de tiempo. Sin embargo, la física continua tiene los medios para descubrir el instante de la colisión en lugar de probar diferentes instantes posibles para ver si ocurrió una colisión.
Por lo tanto, propongo lo siguiente: utilice las técnicas de física continua para determinar cuándo ocurrirá la próxima colisión de cada objeto, con un gran intervalo de tiempo, mucho mayor que el de un solo paso de simulación. Luego, toma el instante de colisión más cercano y descubre dónde estará todo en ese instante.
Sí, eso es mucho trabajo de un solo paso de simulación. Eso significa que la simulación no comenzará instantáneamente ...
... Sin embargo, puede simular los siguientes pasos de simulación sin verificar la colisión cada vez, porque ya sabe cuándo ocurrirá la próxima colisión (o que no se produce una colisión en el gran intervalo de tiempo). Además, los errores acumulados en esa simulación son irrelevantes porque una vez que la simulación alcanza el gran paso de tiempo, simplemente colocamos las posiciones que calculamos de antemano.
Ahora, podemos usar el presupuesto de tiempo que habríamos utilizado para verificar las colisiones en cada paso de simulación para calcular la próxima colisión después de la que encontramos. Es decir, podemos simular con anticipación utilizando el gran paso de tiempo. Suponiendo un mundo de alcance limitado (esto no funcionará para grandes juegos), debe haber una cola de estados futuros para la simulación, y luego cada cuadro que interpolas desde el último estado al siguiente.
Yo abogaría por la interpolación. Sin embargo, dado que hay aceleraciones, no podemos simplemente interpolar todo de la misma manera. En cambio, necesitamos interpolar teniendo en cuenta la aceleración de cada objeto. Para el caso, podríamos actualizar la posición de la misma manera que lo hacemos para el paso de tiempo grande (lo que también significa que es menos propenso a errores porque no estaríamos usando dos implementaciones diferentes para el mismo movimiento).
Nota : Si estamos haciendo estos números de coma flotante, este enfoque no resuelve el problema de los objetos que se comportan de manera diferente cuanto más lejos están del origen. Sin embargo, si bien es cierto que la precisión se pierde cuanto más te alejas del origen, eso sigue siendo determinista. De hecho, es por eso que ni siquiera mencionó eso originalmente.
Apéndice
De OP en el comentario :
La idea es que los jugadores puedan guardar sus máquinas en algún formato (como xml o json), de modo que se registre la posición y rotación de cada pieza. Ese archivo xml o json se usará para reproducir la máquina en la computadora de otro jugador.
Entonces, no hay formato binario, ¿verdad? Eso significa que también debemos preocuparnos de lo que sea que los números de punto flotante recuperados coincidan o no con el original. Ver: precisión de flotación revisada: portabilidad de flotación de nueve dígitos