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.