Ejercicios de emulación: manual de Xbox 360 FMA


Hace muchos años, trabajé en el departamento de Microsoft Xbox 360. Pensamos en lanzar una nueva consola y decidimos que sería genial si esta consola pudiera ejecutar juegos desde la consola de la generación anterior.

La emulación siempre es difícil, pero es aún más difícil si los jefes corporativos cambian constantemente los tipos de procesadores centrales. La primera Xbox (que no debe confundirse con la Xbox One) usó una CPU x86. En la segunda Xbox, es decir, lo siento, la Xbox 360 usaba un procesador PowerPC. La tercera Xbox, es decir, la Xbox One , usaba la CPU x86 / x64. Tales saltos entre diferentes ISA no simplificaron nuestras vidas.

Participé en el trabajo de un equipo que enseñó a la Xbox 360 a emular muchos juegos de la primera Xbox, es decir, emular x86 en PowerPC, y por este trabajo recibí el título de "emulación ninja" . Luego me pidieron que estudiara el problema de emular la CPU Xbox 360 PowerPC en la CPU x64. Diré de antemano que no he encontrado una solución satisfactoria.


FMA! = MMA


Una de las cosas que me molestó fue la fusión múltiple, o las instrucciones de FMA . Estas instrucciones recibieron tres parámetros en la entrada, multiplicaron los dos primeros y luego agregaron el tercero. Fusionado significa que el redondeo no se realizó hasta el final de la operación. Es decir, la multiplicación se realiza con total precisión, después de lo cual se realiza la suma, y ​​solo entonces el resultado se redondea a la respuesta final.

Para mostrar esto con un ejemplo concreto, imaginemos que usamos números decimales de coma flotante y dos dígitos de precisión. Imagine este cálculo, que se muestra como una función:

FMA(8.1e1, 2.9e1, 4.1e1), 8.1e1 * 2.9e1 + 4.1e1, 81 * 29 + 41

81*29 es igual a 2349 y después de sumar 41 obtenemos 2390 . Redondeando hasta dos dígitos, obtenemos 2400 o 2.4e3 .

Si no tenemos FMA, primero tendremos que realizar la multiplicación, obtener 2349 , que redondeará a dos bits de precisión y dará 2300 (2.3e3) . Luego sumamos 41 y obtenemos 2341 , que se redondeará nuevamente y obtendremos el resultado final 2300 (2.3e3) , que es menos preciso que la respuesta FMA.

Nota 1: FMA(a,b, -a*b) calcula el error en a*b , que en realidad es genial.

Nota 2: Uno de los efectos secundarios de la Nota 1 es que x = a * b – a * b puede no devolver cero si la computadora genera automáticamente instrucciones de FMA.

Entonces, obviamente, FMA da resultados más precisos que las instrucciones de multiplicación y suma individuales. No profundizaremos, pero estaremos de acuerdo en que si necesitamos multiplicar dos números y luego sumar el tercero, el FMA será más preciso que sus alternativas. Además, las instrucciones de FMA a menudo tienen menos latencia que la instrucción de multiplicación seguida de la instrucción de suma. En la CPU Xbox 360, la latencia y la velocidad de procesamiento de FMA fueron iguales a las de fmul o fadd , por lo que usar FMA en lugar de fmul seguido de fadd dependiente permitió reducir el retraso a la mitad.

Emulación FMA


El compilador de Xbox 360 siempre ha generado instrucciones de FMA , tanto vectoriales como escalares. No estábamos seguros de que los procesadores x64 que seleccionamos admitieran estas instrucciones, por lo que era fundamental emularlos de forma rápida y precisa. Era necesario que nuestra emulación de estas instrucciones se volviera ideal, porque desde mi experiencia previa de emular cálculos de punto flotante, sabía que los resultados "bastante cercanos" llevaron a que los personajes cayeran por el piso, los autos salieran volando del mundo, etc.

Entonces, ¿qué se necesita para emular perfectamente las instrucciones de FMA si la CPU x64 no las admite?

Afortunadamente, la gran mayoría de los cálculos de coma flotante en los juegos se realizan con precisión flotante (32 bits), y podría usar instrucciones con doble precisión (64 bits) en la emulación FMA.

Parece que emular instrucciones FMA con precisión flotante utilizando cálculos con doble precisión debería ser simple ( voz del narrador: pero no lo es; las operaciones de coma flotante nunca son simples ). Float tiene una precisión de 24 bits, y el doble tiene una precisión de 53 bits. Esto significa que si convierte el flotador entrante a precisión doble (conversión sin pérdida), puede realizar la multiplicación sin errores. Es decir, para almacenar resultados completamente precisos, solo 48 bits de precisión son suficientes, y tenemos más, es decir, todo está en orden.

Entonces necesitamos hacer la suma. Basta con tomar el segundo término en formato flotante, convertirlo al doble y luego agregarlo al resultado de la multiplicación. Dado que el redondeo no ocurre en el proceso de multiplicación, y se realiza solo después de la adición, esto es completamente suficiente para emular FMA. Nuestra lógica es perfecta. Puedes declarar la victoria y volver a casa.

La victoria estuvo tan cerca ...


Pero eso no funciona. O al menos falla para algunos de los datos entrantes. Medita en ti mismo por qué puede suceder esto.

Llamada en espera suena música ...

La falla ocurre porque, según la definición de FMA, la multiplicación y la suma se realizan con total precisión, después de lo cual el resultado se redondea con un flotador de precisión. Casi logramos lograr esto.

La multiplicación ocurre sin redondeo, y luego, después de la adición, se realiza el redondeo. Esto es similar a lo que estamos tratando de hacer. Pero el redondeo después de la adición se realiza con doble precisión. Después de eso, debemos guardar el resultado con precisión flotante, por lo que el redondeo se produce nuevamente.

Pooh Doble redondeo .

Será difícil demostrar esto claramente, así que volvamos a nuestros formatos de coma flotante decimal, donde la precisión simple es dos lugares decimales y la precisión doble es cuatro dígitos. Y imaginemos que calculamos FMA(8.1e1, 2.9e1, 9.9e-1) , o 81 * 29 + .99 .

La respuesta exacta a esta expresión sería 2349.99 o 2.34999e3 . Redondeando a precisión simple (dos dígitos), obtenemos 2.3e3 . Veamos qué sale mal cuando intentamos emular estos cálculos.

Cuando multiplicamos 81 y 29 con una precisión del doble, obtenemos 2349 . Hasta ahora todo bien.

Luego agregamos .99 y obtenemos 2349.99 . Todo sigue bien.

Este resultado se redondea a la precisión del doble y obtenemos 2350 (2.350e3) . Ups

Lo redondeamos al sencillo de precisión y de acuerdo con las reglas de redondeo de IEEE al más cercano, incluso obtenemos 2400 (2.4e3) . Esta es la respuesta incorrecta. Tiene un error ligeramente mayor que el resultado redondeado correctamente devuelto por la instrucción FMA.

Puede indicar que el problema está en la regla del entorno IEEE hasta el par más cercano. Sin embargo, no importa qué regla de redondeo elija, siempre habrá un caso en el que el doble redondeo devuelva un resultado diferente del verdadero FMA.

¿Cómo terminó todo?


No pude encontrar una solución completamente satisfactoria para este problema.

Dejé el equipo de Xbox mucho antes de que se lanzara Xbox One, y desde entonces no he prestado mucha atención a la consola, por lo que no sé qué decisión tomaron. Las CPU modernas x64 tienen instrucciones de FMA que pueden emular perfectamente tales operaciones. También puede usar de alguna manera el coprocesador matemático x87 para emular FMA. No recuerdo a qué conclusión llegué cuando estudié esta pregunta. O tal vez los desarrolladores simplemente decidieron que los resultados son bastante similares y que pueden usarse.

Source: https://habr.com/ru/post/447680/


All Articles