Implementar virtualização usando emulação
- 7 minutos
Agora que entendemos as condições para a virtualização de ISAs e as duas classes principais da virtualização de CPU, a virtualização completa e a paravirtualização, passamos para a discussão de emulação como uma técnica usada para implementar a virtualização completa e processar VMs. Lembre-se de que a emulação é o processo de permitir que as interfaces e as funcionalidades de um sistema (a origem) sejam implementadas em um sistema com interfaces e funcionalidades diferentes (o destino). A emulação é o único mecanismo de virtualização de CPU disponível quando os ISAs convidados e host são diferentes. Se os ISAs convidados e host forem idênticos, a execução nativa direta poderá (possivelmente) ser aplicada.
A emulação é realizada por meio de interpretação ou tradução binária. Com a interpretação, as instruções de origem são convertidas, uma de cada vez, nas instruções de destino relevantes. A interpretação é relativamente lenta devido à emulação um por um de instruções e à falta de qualquer técnica de otimização (por exemplo, impedindo a interpretação de uma instrução já encontrada e interpretada). A tradução binária otimiza a interpretação por meio da conversão de blocos de instruções de origem em instruções de destino e do armazenamento de cache de blocos gerados para uso repetido. Normalmente, blocos de instruções são mais receptivos a otimizações do que instruções únicas. Em comparação com a interpretação, a tradução binária é muito mais rápida devido à aplicação do cache de blocos, bem como as otimizações de código em blocos. No vídeo a seguir, discutiremos três principais esquemas de interpretação: decodificar e despachar, encadeado indireto e encadeado direto.
Conforme explicado no vídeo, um interpretador básico deve ler atentamente a instrução do código-fonte por instrução, analisar cada instrução e chamar rotinas relevantes para gerar o código de destino. Um interpretador chamado decodificação e despacho aplica interpretação básica, mas resulta em várias instruções de branch (ou salto), diretas e indiretas, que leva a tempos de execução não satisfatórios. Como uma otimização para o tipo decodificar e despachar, um interpretador chamado indirect threaded tenta liberar alguns branches de decodificação e despacho por meio da anexação (ou threading) de uma parte do código de despacho ao final de cada rotina do interpretador. Por fim, um interpretador mais avançado, chamado direct threaded, aprimora o indirect threaded tentando interpretar uma operação repetida apenas uma vez. Embora o interpretador direct threaded aprimore as os branches indirect threaded e de decodificação e despacho, ela ainda sofre grandes desvantagens, como a vasta imagem de memória e portabilidade limitada. No próximo vídeo, abordaremos a tradução binária, que basicamente se destina às limitações da interpretação direct threaded.
Conforme apresentado neste vídeo, a tradução binária tenta amortizar os custos de busca e análise causados pelo interpretador direto por meio de: (1) tradução de um bloco de instruções de origem (em vez de uma única instrução) para um bloco de instruções de destino e (2) armazenamento em cache do código traduzido, na tentativa de evitar a interpretação das instruções de origem mais de uma vez. A seguinte tabela compara as técnicas de emulação de tradução binária, decodificação e despacho, indirect threaded e direct threaded em termos de quatro métricas: requisitos de memória, desempenho de inicialização, desempenho de estado estável e portabilidade de código. Por exemplo, a linha do interpretador de decodificação e despacho lê da seguinte maneira: primeiro, com a decodificação e o despacho, os requisitos de memória permanecem baixos devido à existência de apenas uma rotina do interpretador para cada tipo de instrução no ISA de destino. Além disso, a decodificação e o despacho evita o threading do código de despacho no final de cada rotina. Em segundo lugar, o desempenho de inicialização é rápido porque nem a pré-decodificação nem a tradução do binário de origem é aplicada. Em terceiro lugar, o desempenho de estado estável (ou seja, o desempenho depois de iniciar o interpretador) é lento devido ao grande número de branches e à interpretação de cada instrução para cada aparência. Por fim, a portabilidade de código é boa porque a pré-decodificação com endereços de rotinas do interpretador não é aplicada por interpretadores de decodificação e despacho (em oposição a interpretadores direct threaded).
| Requisitos de memória | Desempenho da inicialização | Desempenho de estado estável | Portabilidade do código | |
|---|---|---|---|---|
| Interpretador de decodificação e expedição | Baixo | Rápido | Lento | Bom |
| Interpretador de thread indireto | Baixo | Rápido | Lento | Bom |
| Interpretador de thread direto | Alto | Lento | Médio | Médio |
| Tradução binária | Alto | Muito lento | Rápido | Ruim |