Precisão, Velocidade e Otimização da Emulação
1 Texto
Um tópico comumente discutido em emulação é a "precisão" de um emulador. O termo significa o quão perto a emulação está do hardware original. Porém, existe um montante significativo de complexidade escondido neste simples termo. Não existe uma métrica simples acerca do que torna um emulador "preciso". Imagina-se que precisão em emuladores implica ser mais lento mas com menos bugs. Diz-se geralmente que emuladores menos precisos são mais rápidos e "bons o bastante" para a maioria dos jogos. Enquanto há um núcleo de verdade nessas alegações, há bem mais sobre a realidade do assunto.
Um dos termos mais prevalentes usados para descrever a precisão da emulação é a "precisão por ciclo". O termo tem um significado específico, mas é geralmente confundido e aplicado de maneira hiper-abrangente. precisão por ciclo, grosso modo, significa que cada aspecto singular do sistema emulado ocorre no tempo correto relativo a todo o restante. Para muitos sistemas com temporação estreita e acesso mais direto ao hardware, especialmente sistemas mais antigos, precisão por ciclo é um aspecto chave da emulação altamente precisa.
A palavra "ciclo" neste termo refere-se à unidade fundamental de timing na lógica digital: o ciclo de relógio. As quantidades comumente discutidas em MHz e GHz na descrição de sistemas e processadores referem-se à frequência do relógio naquele sistema. Portanto, para um sistema que tem um processador de 16MHz, tam como o Game Boy Advance, isto significa que o processador executa 16 milhões de ciclos por segundo. Outra peça importante de hardware, o barramento, algumas vezes tem uma taxa de relógio diferente. Um barramente é uma interconexão que transporta dados entre vários componentes do sistema, tais como a CPU e a memória principal. Enquanto o barramento geralmente executa na mesma velocidade da CPU, como ocorre no GBA, isto não é uma garantia: no Nintendo DS, há dois precssadores, o ARM& (o mesmo do GBA) e o ARM9, que rodam em velocidades distintas (aproximadamente 33MHz e 67MHz respectivamente), porém o barramento roda nos mesmos 33MHz do ARM7. Como resultado, o ARM9 pode ter que esperar o barramento mais lento obter dados da memória que ele poderia obter mais velozmente se o barramento tivesse a mesma velocidade da taxa de relógio do ARM9.
Precisão por contagem de ciclos é um conceito similar, mas em vez de cada peça de hardware ser emulada no ciclo correto em relação a outros componentes que atuam concorrentemente, cada componenete atua atomicamente e leva o montante correto de tempo, mas não pode intercalar propriamente com o timing de outro hardware. Assim sendo, precisão por contagem de ciclos pode parecer estritamente inferior à precisão por ciclo, e de uma perspectiva de uma precisão perfeita de hardware, este é o caso. Porém, precisão por contagem de ciclo é um estilo de emulação muito mais fácil de projetar, implementar, e manter. É um desentendimento comum que o mGBA é ou será preciso em ciclos, mas fazer isso exigiria uma reescrita imensa de alguns dos elementos fundacionais do mGBA. Quando bem implementada, precisão por contagem de ciclo produzirá resultados bastante semelhantes, e comumente idênticos.
O problema principal que a precisão por ciclo resolve é emular corretamente diferentes peças de hardware realizando ações no mesmo ciclo. Isto pode parecer uma tarefa fácil: realizar todos os passos individuais que acontecem em um dado ciclo, na ordem, seguir para o ciclo seguinte, e repetir. Isto pode tornar-se bastante lento, e introduz as principais diferenças no desempenho entre projetos de precisão por ciclo e por contagem de ciclo. O hardware realiza todos esses passos independentemente em qualquer ciclo dado. O software nao é capaz de realizar estes passos em paralelo e deve intercalar ente as operações. Isto é computacionalmente caro e portanto lento. Vamos observar um exemplo.
Assuma por um momemto que você tenha uma CPU simples onde uma operação na CPU sempre leva dois ciclos, e a GPU lê um valor da memória para um pixel a cada 1 ciclo. Em teoria, isto é emulado um ciclo por vez. Realize a primeira metade da operação da CPU, então faça um traçado de pixel da GPU. Em seguida, faça a segunda metade da operação da CPU, e faça outro traçado de pixel. Mas existem muitas complexidades ocultas por detrás desta teoria. Você tem que saber qual parte da instrução da CPU está acontecendo em cada ciclo dado, o que geralmente é dependente de implementação e pobremente definido. Você também tem que ser capaz de armazenar uma operação semi-finalizada a fim de retornar a ela depois. Isto imnplica complexidade adicional sobre um projeto atômico, resultando em um golpe no desempenho. Em um projeto atômico, as operações não são divididas por ciclo. Em vez disso, cada operação executa até seu término, e daí mais operações podem ser realizadas. Porém, se você coloca a ordem das coisas erroneamente, podem ocorrer consequências visíveis. Um exemplo é quando o acesso à memória pode interferir com as operações da GPU, o timing incorreto pode resultar em gráficos incorretos.
Apesar destas questões, operações atômicas são a base da precisão por contagem de ciclos. Quando lidando com diferentes peças de hardware, precisão por contagem de ciclos tem as instruções individuais tomando o número correto de ciclos, e operações capazes de aparentar como se estivessem ocorrendo no passado. Combinados, estes criam uma aproximação boa, ainda que não perfeita, da precisão por ciclos. Em um modelo de precisão por contagem de ciclos, as instruções da CPU não podem ser preemptadas ou interrompidas, e outras peças de operações de hardware são realizadas entre as instruções da CPU. Porém, a fim de ser capaz de agendar estas partezinhas de hardware para surgir no passado, eles podem ainda levarem todos o mesmo montante de tempo. O principal revés desta abordagem é que operações concorrentes que interagiriam no hardware original não podem ser apropriadamente entrelaçadas. Porém, dependendo da idade e complexidade do sistema, tais interações podem ser excessivamente raras. Enquanto sistemas antigos são lotados de casos limítrofes e interações complexas, sistemas mais recentes são mais cuidadosamente projetados e contêm proteções que evitam a ocorrência de tais interações afinal. Isto torna sistemas mais recentes um alvo muito mais atrativo para precisão por contagem de ciclos. É mais fácil e quase sempre "é boa o suficiente".
Mas então, o que qualifica-se como "boa o suficiente"? Este é um assunto subjetivo e contencioso. Se um jogo é jogável sem bugs significativos e óbvios, a maioria dos jogadores pode dizer que ele é bom o bastante. Porém, para jogadores de verlocidade (speedruners) e TAS 1, qualquer falta de precisão é um problema. Por muito tempo o ZSNES, ainda que loucamente impreciso, foi considerado por uma larga parte da comunidade de emulação de SNES como sendo "bom o suficiente". Mesmo com baixa precisão, muitos jogos rodavam quase perfeitamente. Sempre havia casos em que o ZSNES falhava completamente, mas a maioria dos jogos populares era emulado bem o suficiente de tal modo que aumentar a precisão não era uma prioridade. Para muitas pessoas, ele era "bom o suficiente". Para algumas, ainda é. Mas está longe de ser perfeito, e isto ficou ruim para algumas pessoas.
Isto leva à criação daquele que é mais bem-conhecido exemplo de emulador preciso por ciclos: higan (antigo bsnes). Ele é legendariamente preciso, mas também famigeradamente lento. Isto é em parte devido ao fato de ser preciso por ciclos. Ele usa co-rotinas para trocar entre porções da emulação quando necessário, o que tem um monte de sobrecarga. Como higan é o melhor exemplo conhecido de emulador preciso por ciclos, ele tem levado à má concepção que precisão de ciclo é necessariamente extremamente lenta. Porém, muitas das questões de desempenho do higan são porque o emulador não é otimizado para velocidade. Esta foi uma decisão intencional da parte de byuu a fim de assegurar que o código seja ultimamente legível e compreensível, já que byuu mantém uma política estrita de código-como-documentação. Além de ser um meio de jogar jogos do SNES, byuu trata o higan como um projeto de preservação e documentação do comportamento do próprio SNES. É possível fazer um emulador de SNES altamente otimizado e preciso que seria significativamente mais rápido que higan sem sacrificar muito da precisão; porém, ninguém fez isso. É uma tarefa bastante intimidadora, afinal.
Quando eu comecei a trabalhar no mGBA, meus objetivos para ele eram que ele fosse mais preciso que o VisualBoyAdvance, e também mais rápido. Enquanto estes dois objetivos estão comumente em oposição, há muito mais sobre a precisão que apenas o timing. Algumas vezes ela chega em questões como renderização incompleta de gráficos, ou operações inválidas de memória não sendo apropriadamente emuladas. Eu descobri que o VisualBoyAdvance tinha muitas áreas em que faltava grande otimização, e muitas das melhorias requeridas na precisão não impactavam a velocidade. Com velocidade aumentada, eu também tive sobrecarga que podia usar para fazer melhorias de desempenho que impactavam a velocidade.
Ao contrário da emulação GBA, a emulação do Game Boy no mGBA (também conhecida como mGB), é projetada com precisão de ciclo em mente. A emulação da instrução é dividida em tarefas que ocorrem em ciclos individuais do relógio e entre essas operações outro hardware pode ser emulado. Porém, ao longo de muitas otimizações, tais como preparar um lote de operações em vez derodá-las uma por vez (e separar um lote assim que necessário quando há interações concorrentes), mGB é bastante rápido. É bem mais rápido que uma implementação com precisão de ciclo pode ser, sem sacrificar a velocidade.
Um exemplo notável onde precisão de ciclo é um impedimento é na futura emulação da fila de comandos FIFO da GPU do DS. Quando se escrevem comandos para a GPU, o DS constrói uma lista de comandos que ainda não foram processados. Novos comandos acabam anexados ao fim desta lista. Porém, a FIFO tem um tamanho máximo. Quando ela lota, a escrita bloqueia até a FIFO ter espaço suficiente para os novos comandos. Em hardware real, se a FIFO está cheia, o barramento de memória obstrui, causando a CPU ARM a bloquear temporariamente no meio desta instrução. A FIFO é então lida pela GPU independentemente do barramento de memória, e finalmente o barramento de memória pode prosseguir. Dado que operações de CPU em medusa são tratadas como atômicas,
Como as operações de CPU em medusa são tratadas como atômicas, obstruir o barramento de memória e processar a FIFO na GPU no meio de uma instrução não é possível. Em vez disso, a maneira pela qual medusa lida com a escrita de uma FIFO cheia é salvando2 o valor a ser escrito e informando à CPU que ela não pode executar novas instruções até a FIFO ter espaço e o valor salvo ser descarregado na FIFO. Porém, a CPU ARMtem um conjunto de instruções que mermite escrever mais de um valor na memória a um tempo. Isto significa que é possível escrever mais de um comando na FIFO em uma simples instrução e o barramento de memória pode obstruir entre as escritas: e.g. se você escreve três comandos na FIFO, mas só cabe um, ela vai obstruir antes do terceiro sequer tentar escrever. Isto pôe um grande problema para medusa. A abordagem atual, que na realidade viola a precisão de contagem de ciclos, é processar o suficiente da FIFO imediatamente para escrever novos comandos. Este é um caso limítrofe específico, mas é um comportamento incorreto e que precisará ser abordado.
Indo ainda mais além da precisão de contagem de ciclos, está o conceito de emulação de alto nível, ou HLE 3. Muitos sistemas de videogame, especialmente desde o final dos anos '90, têm componentes programáveis que não são parte do software emulado em si mesmo: os componentes são parte do sistema em si e não têm diferenças majoritárias entre os jogos. Alguns exemplos são os DSPs (como no GameCube), software de sistema (como o PSP e o 3DS) e dispositivos programáveis de microcódigo como RSP no Nintendo 64. Enquanto estes componentes podem ser diretamente emulados (referidos como emulação de baixo nível ou LLE 4), é de fato possível livrar-se ao não emulá-los passo a passo, instrução a instrução. Ao escrever uma implementação customizada do componente que que tenha o mesmo efeito visível do hardware, mas executa como código nativo, em vez de emulado passo a passo, é possível acelerar significativamente a operação. Um dos reveses é que a sincronização com diferentes componentes de hardware e uma temporação apropriada tornam-se quase impossíveis. Além disso, implementações HLE exigem quantias signiicativas de pesquisa não apenas do próprio hardware, mas também do microcódigo que roda neste hardware. Implementações HLE pioneiras geralmente são crivadas de bugs que não estariam presentes em LLE, e podem ser bem difíceis de depurar. Implementações LLE, porém, exigem cópias do código a ser emulado, o quie nem sempre é difícil de despejar, e não podem ser distribuídas junto ao emulador por conta de direitos de cópia.
Contrapesos entre precisão e velocidade são uma proposição difícil. Para sistemas mais antigos, precisão é quase sempre preferível dado que eles já são bastante rápidos para emular e geralmente contêm restrições mais apertadas de timing. Com sistemas modernos, HLE é praticamente requisito para emular o sistema afinal. É um balanceamento difícil, e há vantagens de ambos os lados. Em geral, precisão terá menos problemas no sistema emulado e será mais valioso para preservação, mas para velocidade e emulação de plataformas recentes, precisão não é sempre uma necessidade.
2 META
- Autor: endrift
- Link: Emulation Accuracy, Speed, and Optimization
- Arquivo: http://archive.is/V9WtM