Interpretadores voltaram a ser um assunto muito discutido devido aos resultados alcansados pela SquirrelFish (interpretador de javascript do Webkit) e ao fato da VM do Android também usar um. Curiosamente, ambas VMs são baseadas em registradores em vez de pilha, como as máquinas virtuais tradicionais como JVM e CLR.
A grande diferença está na forma como valores intermediários são calculados. Em uma VM baseada em pilha, operandos são adicionados ao topo da pilha enquanto operadores os retiram e realizam alguma operação. Uma máquina baseada em registradores usa de variaveis para armazenar resultados intermediários.
Para exemplificar melhor, o código “a = b * c + 10″ ficaria algo como:
Máquina de pilha:
push_local 'b' push_local 'c' mul push_const 10 add store_local 'a'
Máquina de registradores:
load_local R0 <= 'b' load_local R1 <= 'c' mul R2 <= R0, R1 load_const R3, 10 add R4 <= R2, R3 store_local 'a' <= R4
A primeira vista a diferença é quase que estética, porém se considerarmos quanto espaço precisamos para representar o código e o número de operações de leitura e escrita à memória cada uma faz, vamos notar como são técnicas fundamentalmente diferentes.
Vamos assumir um formato bem simples para representar o código de ambas, que é um byte para designar a operação seguido de um byte para cada operando. Aplicando esta forma, temos.
Máquina de pilha:
push_local 'b' //2 bytes push_local 'c' //2 bytes mul //1 byte push_const 10 //2 bytes add //1 byte store_local 'a' //2 bytes
Total: 10 bytes
Máquina de registradores:
load_local R0 <= 'b' //3 bytes load_local R1 <= 'c' //3 bytes mul R2 <= R0, R1 //4 bytes load_const R3 <= 10 //3 bytes add R4 <= R2, R3 //4 bytes store_local 'a' <= R4 //3 bytes
Total: 20 bytes
Fica claro aqui que uma máquina de pilha admite uma representação muito mais compacta do mesmo programa. Em geral essa relação não é tão gritante, pois existe uma série de formas de diminuir o número de instruções necessárias para uma máquina de registradores.
Agora vamos analisar quantas operações de memória cada máquina faz, verificando cada instrução individualmente.
Maquina de pilha:
push_local X
- lê a variavel local X
- lê o endereço atual do topo da pilha
- escreve X no topo da pilha
- incrementa e armazena o novo valor para topo da pilha
mul / add
- lê o endereço atual do topo da pilha
- lê o valor do topo da pilha
- lê o valor abaixo do topo da pilha
- escreve o resultado no local abaixo do topo da pilha
- decrementa e armazena o novo valor para topo da pilha
push_const X
- lê o endereço atual do topo da pilha
- escreve X no topo da pilha
- incrementa e armazena o novo valor para topo da pilha
store_local X
- lê o endereço atual do topo da pilha
- lê o valor no topo da pilha
- escreve o valor em X
- decrementa e armazena o novo valor para topo da pilha
Aplicando esses valores ao programa em questão temos um total de 25 operações.
Máquina de registradores:
load_local X <= Y
- lê o valor da variavel local Y
- escreve o valor no registrador X
mul / add X <= Y, Z
- lê o valor do registrador local Y
- lê o valor do registrador local Z
- escreve o valor no registrador X
load_const X <= Y
- escreve o valor de Y no registrador X
store_local X <= Y
- lê o valor do registrador Y
- escreve o valor no registrador X
Fazendo a conta, temos um total de 13 operações.
Ironicamente neste caso, uma máquina de registradores leva uma significante vantagem em relação a uma máquina de pilha. Apesar de se tratar de uma simplificação, na prática o resultado é o mesmo. Em máquinas modernas onde o custo de acessar a memória principal é muito grande, uma máquina de registradores tem o potencial de ser mais rápida.
Essas dicotomia entre qual admite uma representação mais compacta e qual permite uma implementação mais eficiente é fundamental na arquitetura de qualquer máquina virtual. Apesar disso, existe muito além da simples forma de funcionar do interpretador que define a real performance da VM, ainda mais se falarmos de linguagens dinâmicas. Em um artigo futuro pretendo mostrar uma implementação de exemplo de ambos os tipos de máquinas virtuais.
2 responses so far ↓
1 Wagner Elias - Nova onda de browsers e as máquinas virtuais // Sep 30, 2008 at 5:00 pm
[...] as principais mudanças está a transformação de javascript direto para linguagem de máquina. Este post do Rodrigo Kumpera mostra uma outra característica, agora os interpretadores são baseados em registradores e não em [...]
2 felipe tonello // Feb 18, 2009 at 5:23 pm
Cara, muito bom!
Eu tenho muito interesse em compiladores e interpretadores. Fiz um interpretador de expressões matemáticas usando pilhas, para brincar. Não sabia que existia essa técnica de usar registros.
Continue com seus artigos, são ótimos, apesar de não concordar quando você crítica *tanto* C ou C++ =P Eu sei disso pois acompanho na ccppbrasil hehe
Abraços
Leave a Comment