¿Coaccionar al punto flotante para que sea determinista en .NET?

He estado leyendo mucho sobre el determinismo de punto flotante en .NET, es decir, asegurándome de que el mismo código con las mismas entradas dará los mismos resultados en diferentes máquinas. Como .NET carece de opciones como fpstrict de Java y fp de MSVC: estrictas, el consenso parece ser que no hay forma de evitar este problema usando un código administrado puro. El juego de C # AI Wars se ha basado en el uso de matemáticas de punto fijo , pero esta es una solución engorrosa.

El problema principal parece ser que el CLR permite que los resultados intermedios vivan en registros FPU que tienen una precisión mayor que la precisión nativa del tipo, lo que lleva a resultados de precisión impredictably más altos. Un artículo de MSDN por el ingeniero de CLR David Notario explica lo siguiente:

Tenga en cuenta que con la especificación actual, todavía es una opción de idioma para dar ‘previsibilidad’. El lenguaje puede insertar instrucciones conv.r4 o conv.r8 después de cada operación FP para obtener un comportamiento ‘predecible’. Obviamente, esto es realmente costoso, y diferentes idiomas tienen diferentes compromisos. C #, por ejemplo, no hace nada, si desea reducir, tendrá que insertar (flotante) y (doble) moldes a mano.

Esto sugiere que uno puede alcanzar el determinismo de coma flotante simplemente insertando moldes explícitos para cada expresión y sub-expresión que evalúe flotar. Uno podría escribir un tipo de envoltorio alrededor del flotador para automatizar esta tarea. ¡Esta sería una solución simple e ideal!

Sin embargo, otros comentarios sugieren que no es tan simple. Eric Lippert declaró recientemente (el énfasis es mío):

en alguna versión del tiempo de ejecución, la conversión a flotación explícitamente da un resultado diferente que no hacerlo. Cuando expulsa explícitamente a flotación, el comstackdor de C # da una pista al tiempo de ejecución para decir “saque esto del modo de muy alta precisión si está utilizando esta optimización”.

¿Qué es esta “pista” para el tiempo de ejecución? ¿La especificación de C # estipula que un molde explícito para flotar causa la inserción de un conv.r4 en el IL? ¿La especificación CLR estipula que una instrucción conv.r4 hace que un valor se reduzca a su tamaño original? Solo si ambos son verdaderos, podemos confiar en conversiones explícitas para proporcionar “predictibilidad” en coma flotante como lo explica David Notario.

Finalmente, incluso si podemos coaccionar todos los resultados intermedios al tamaño nativo del tipo, ¿es esto suficiente para garantizar la reproducibilidad entre las máquinas, o hay otros factores como la configuración de tiempo de ejecución de FPU / SSE?

¿Qué es esta “pista” para el tiempo de ejecución?

A medida que conjeturas, el comstackdor rastrea si una conversión a doble o flotante estaba realmente presente en el código fuente, y si lo era, siempre inserta el opcode apropiado.

¿La especificación de C # estipula que un molde explícito para flotar causa la inserción de un conv.r4 en el IL?

No, pero te aseguro que hay pruebas unitarias en los casos de prueba del comstackdor que aseguran que así sea. Aunque la especificación no lo exige, puede confiar en este comportamiento.

El único comentario de la especificación es que cualquier operación de coma flotante puede realizarse con una precisión mayor que la requerida al capricho del tiempo de ejecución, y que esto puede hacer que sus resultados sean inesperadamente más precisos. Ver la sección 4.1.6.

¿La especificación CLR estipula que una instrucción conv.r4 hace que un valor se reduzca a su tamaño original?

Sí, en la Partición I, sección 12.1.3, que observo que podría haber buscado usted mismo en lugar de pedirle a Internet que lo haga por usted. Estas especificaciones son gratuitas en la web.

Una pregunta que no hizo, pero probablemente debería tener:

¿Hay alguna operación que no sea de fundición que trunca los flotadores fuera del modo de alta precisión?

Sí. Asignando a un campo estático, campo de instancia o elemento de una matriz double[] o float[] trunca.

¿El truncamiento constante es suficiente para garantizar la reproducibilidad en todas las máquinas?

No. Le animo a leer la sección 12.1.3, que tiene mucho que decir sobre el tema de los denormales y NaN.

Y finalmente, otra pregunta que no hizo, pero probablemente debería tener:

¿Cómo puedo garantizar una aritmética reproducible?

Usa enteros.

El diseño del chip de la unidad de punto flotante 8087 fue el error de mil millones de dólares de Intel. La idea se ve bien en el papel, le da una stack de 8 registros que almacena valores en precisión extendida, 80 bits. Para que pueda escribir cálculos cuyos valores intermedios tienen menos probabilidades de perder dígitos significativos.

La bestia es sin embargo imposible de optimizar para. Almacenar un valor de la stack de FPU en la memoria es costoso. Por lo tanto, mantenerlos dentro de la FPU es un gran objective de optimización. Inevitable, tener solo 8 registros requerirá una reescritura si el cálculo es lo suficientemente profundo. También se implementa como una stack, registros no direccionables por lo que también requiere gimnasia que puede producir una reescritura. Inevitablemente, una escritura revertida truncará el valor de 80 bits a 64 bits, perdiendo precisión.

Entonces, las consecuencias son que el código no optimizado no produce el mismo resultado que el código optimizado. Y pequeños cambios en el cálculo pueden tener grandes efectos en el resultado cuando un valor intermedio termina necesitando ser escrito. La opción / fp: strict es un hack alrededor de eso, obliga al generador de código a emitir un write-back para mantener los valores consistentes, pero con la inevitable y considerable pérdida de perf.

Esta es una roca completa y un lugar difícil. Para el jitter x86, simplemente no intentaron resolver el problema.

Intel no cometió el mismo error cuando diseñaron el conjunto de instrucciones SSE. Los registros XMM son direccionables libremente y no almacenan bits adicionales. Si desea resultados consistentes, entonces la comstackción con el objective AnyCPU y un sistema operativo de 64 bits es la solución rápida. El jitter x64 usa SSE en lugar de instrucciones FPU para matemática de coma flotante. Si bien esto agregó una tercera forma, un cálculo puede producir un resultado diferente. Si el cálculo es incorrecto porque pierde demasiados dígitos significativos, será sistemáticamente incorrecto. Lo cual es un poco bromuro, en realidad, pero por lo general solo en lo que respecta a un progtwigdor.