Muitos anos atrás, trabalhei no departamento Microsoft Xbox 360. Pensamos em lançar um novo console e decidimos que seria ótimo se esse console pudesse rodar jogos do console da geração anterior.
A emulação é sempre difícil, mas é ainda mais difícil se seus chefes corporativos estiverem constantemente mudando os tipos de processadores centrais. O primeiro Xbox (que não deve ser confundido com o Xbox One) usava uma CPU x86. No segundo Xbox, desculpe, o Xbox
360 usava um processador PowerPC. O terceiro Xbox, ou seja, o Xbox
One , usava a CPU x86 / x64. Tais saltos entre diferentes
ISAs não simplificaram nossas vidas.
Participei do trabalho da equipe que ensinou o Xbox 360 a emular muitos jogos do primeiro Xbox, ou seja, emular x86 no PowerPC, e para este trabalho recebi o título de
“emulação ninja” . Fui convidado a estudar a questão da emulação da CPU Xbox 360 PowerPC na CPU x64. Direi antecipadamente que não encontrei uma solução satisfatória.
FMA! = MMA
Uma das coisas que me incomodou foi o multiply add fundido, ou instruções
FMA . Essas instruções receberam três parâmetros na entrada, multiplicaram os dois primeiros e adicionaram o terceiro. Fundido significava que o arredondamento não era realizado até o final da operação. Ou seja, a multiplicação é realizada com precisão total, após a qual a adição é realizada e somente então o resultado é arredondado para a resposta final.
Para mostrar isso com um exemplo concreto, vamos imaginar que usamos números decimais de ponto flutuante e dois dígitos de precisão. Imagine este cálculo, mostrado como uma função:
FMA(8.1e1, 2.9e1, 4.1e1), 8.1e1 * 2.9e1 + 4.1e1, 81 * 29 + 41
81*29
é igual a
2349
e, depois de adicionar 41, obtemos
2390
. Arredondando até dois dígitos, obtemos
2400
ou
2.4e3
.
Se não tivermos FMA, primeiro teremos que realizar a multiplicação, obter
2349
, que arredondará até dois bits de precisão e fornecerá
2300 (2.3e3)
. Em seguida, adicionamos
41
e obtemos
2341
, que
serão arredondados
novamente e obteremos o resultado final
2300 (2.3e3)
, que é menos preciso do que a resposta das FMA.
Nota 1: FMA(a,b, -a*b)
calcula o erro em a*b
, o que é realmente interessante.
Nota 2: Um dos efeitos colaterais da Nota 1 é que x = a * b – a * b
pode não retornar zero se o computador gerar automaticamente instruções FMA.
Portanto, obviamente, a FMA fornece resultados mais precisos do que as instruções individuais de multiplicação e adição. Não nos aprofundaremos, mas concordaremos que, se precisarmos multiplicar dois números e depois adicionar o terceiro, as FMA serão mais precisas do que suas alternativas. Além disso, as instruções FMA geralmente têm menos latência do que a instrução de multiplicação seguida pela instrução de adição. Na CPU do Xbox 360, a latência e a velocidade de processamento do FMA eram iguais às do
fmul ou
fadd ; portanto, usar o FMA em vez de
fmul seguido pelo
fadd dependente permitiu reduzir o atraso pela metade.
Emulação FMA
O compilador do Xbox 360
sempre gerou
instruções de FMA , vetoriais e escalares. Não tínhamos certeza de que os processadores x64 que selecionamos suportariam essas instruções, por isso era fundamental imitá-las com rapidez e precisão. Era necessário que nossa emulação dessas instruções se tornasse ideal, porque da minha experiência anterior emular cálculos de ponto flutuante, eu sabia que resultados "razoavelmente próximos" resultavam em caracteres caindo pelo chão, carros voando para fora do mundo e assim por diante.
Então, o que é
necessário para emular perfeitamente as instruções FMA se a CPU x64 não as suportar?
Felizmente, a grande maioria dos cálculos de ponto flutuante nos jogos é realizada com precisão de flutuação (32 bits), e eu poderia felizmente usar instruções com precisão dupla (64 bits) na emulação de FMA.
Parece que emular instruções FMA com precisão de flutuação usando cálculos com precisão dupla deve ser simples (
voz do narrador: mas não é; operações de ponto flutuante nunca são simples ). O flutuador tem uma precisão de 24 bits e o dobro tem uma precisão de 53 bits. Isso significa que, se você converter a flutuação de entrada em precisão dupla (conversão sem perdas), poderá executar a multiplicação sem erros. Ou seja, para armazenar resultados completamente precisos, apenas 48 bits de precisão são suficientes e temos mais, isto é, tudo está em ordem.
Então precisamos fazer a adição. Basta pegar o segundo termo no formato flutuante, convertê-lo para o dobro e adicioná-lo ao resultado da multiplicação. Como o arredondamento não ocorre no processo de multiplicação e é realizado somente após a adição, isso é suficiente para emular FMA. Nossa lógica é perfeita. Você pode declarar vitória e voltar para casa.
A vitória estava tão perto ...
Mas isso não funciona. Ou, pelo menos, falha em alguns dos dados recebidos. Reflita sobre por que isso pode acontecer.
A chamada retém sons de música ...
A falha ocorre porque, pela definição de FMA, a multiplicação e a adição são realizadas com precisão total, após o que o resultado é arredondado com um flutuador de precisão. Nós
quase conseguimos isso.
A multiplicação ocorre sem arredondamento e, depois da adição, o arredondamento é realizado. Isso é
semelhante ao que estamos tentando fazer. Mas o arredondamento após a adição é feito com precisão
dupla . Depois disso, precisamos salvar o resultado com precisão de flutuação, e é por isso que o arredondamento ocorre novamente.
Pooh
Arredondamento duplo .
Será difícil mostrar isso claramente, então vamos voltar aos nossos formatos decimais de ponto flutuante, onde a precisão única é de duas casas decimais e a precisão dupla de quatro dígitos. E vamos imaginar que calculamos as
FMA(8.1e1, 2.9e1, 9.9e-1)
ou
81 * 29 + .99
.
A resposta exata para essa expressão seria
2349.99
ou
2.34999e3
. Arredondando para precisão simples (dois dígitos), obtemos
2.3e3
. Vamos ver o que está errado quando tentamos emular esses cálculos.
Quando multiplicamos
81
e
29
com uma precisão de dobro, obtemos
2349
. Até agora tudo bem.
Em seguida, adicionamos
.99
e obtemos
2349.99
. Ainda está tudo bem.
Este resultado é arredondado para a precisão do dobro e obtemos
2350 (2.350e3)
. Opa
Arredondamos para a precisão simples e de acordo com
as regras de
arredondamento da IEEE
para a mais próxima, mesmo com
2400 (2.4e3)
. Esta é a resposta errada. Ele tem um erro um pouco maior que o resultado arredondado corretamente retornado pela instrução FMA.
Você pode indicar que o problema está na regra do ambiente IEEE até a mais próxima. No entanto, independentemente da regra de arredondamento escolhida, sempre haverá um caso em que o arredondamento duplo retorna um resultado diferente do verdadeiro FMA.
Como tudo terminou?
Não consegui encontrar uma solução completamente satisfatória para esse problema.
Deixei a equipe do Xbox muito antes do lançamento do Xbox One e, desde então, não presto muita atenção ao console, então não sei qual decisão eles tomaram. As CPUs modernas x64 possuem instruções FMA que podem emular perfeitamente essas operações. Você também pode usar o coprocessador matemático x87 para emular FMA - não me lembro a que conclusão cheguei quando estudei essa pergunta. Ou talvez os desenvolvedores tenham simplesmente decidido que os resultados são razoavelmente próximos e podem ser usados.