Geração de Código Intermediário
Como prometido, agora irei explanar de forma sucinta e
objetiva (e provavelmente entendível) como funciona a fase de geração de código intermediário, onde
o compilador agora transformará todos as representações criadas (se você ainda não
leu sobre essas representações dá uma olhada no meu último post) em uma única forma intermediária, como se
fosse um programa para um computador abstrato (como fazemos algoritmos em pseudocódigo)
, tendo como objetivo criar uma forma cuja tradução
para o código final seja fácil.
A representação é criada numa linguagem próxima ao assembly, mas diferente da
geração de código final, independe do
processador pois os compiladores não trabalham com o código específico de um
processador nesta fase, as formas usuais para essa representação são a notação posfixa e o código de três endereços.
O código de três
endereços é uma forma de representação assim chamada pois consiste em
sequências parecidas com instruções assembly, utilizando apenas três variáveis
por instrução, onde cada operador pode atuar como um registro, é construída geralmente
da forma z = x op y, sendo utilizada uma variável para armazenar o resultado e
outros dois operandos e sua respectiva operação do outro lado. Por exemplo:
a
= c + d * b
Essa expressão seria traduzida da seguinte forma:
t1
= d * b
a
= c + t1
Como você
pôde perceber as instruções são separadas na sequência em que serão executadas e
necessariamente são criadas variáveis temporárias (t1) para transladar o código. O código de três endereços possui
alguns pontos fracos como utilizar não mais que uma operação por vez, de ordem
fixa, e ainda há a necessidade de criação de variáveis temporárias. Além das
operações binárias podem ser feitas operações de atribuições, chamada de
funções, instruções de desvio.
A notação posfixa (ou notação polonesa reversa (RPN em inglês)),
diferente da aritmética normal, que utiliza seus operadores entre os elementos (chamada notação infixa), coloca as operações depois dos operandos não
havendo por exemplo a necessidade de parênteses.
Tudo fica mais compreensível com exemplos:
Notação infixa:
(a + b) * c
Notação posfixa:
a b + c *
Esse tipo de notação é muito
utilizado quando se trabalha com pilhas (stack) e também internamente em
linguagens de programação, tem-se a vantagem de realizar cálculos de maneira
mais eficiente já que não há necessidade de verificar a precedência de
operadores, já que depois de colocados na pilha e calculado um novo elemento
será inserido, ex:
push a
push b
add a,b
pop a
pop b
push _result
push c
mult _result, c
pop c
Otimização de Código
A
otimização de código é uma fase opcional
na compilação, podendo ocorrer em duas fases, a primeira é depois de ser
gerado o código intermediário, quando o compilador melhora o próprio código removendo
registros e operações desnecessárias ou as substituindo por outra mais
eficiente, a outra fase é na geração do código final. A diferença entre essas
duas fases é que a primeira não depende
do processador já que o código que sofre otimizações é o intermediário, já
a segunda fase depende da máquina alvo pois
o código final (código assembly ou de máquina) será exclusivo para aquela
arquitetura. A otimização leva um tempo significativo para ser aplicada, sendo que nem sempre vale a
pena o tempo consumido.
Geração de Código
A última fase de compilação, aqui o compilador mapeia a
representação intermediária do código no código alvo, se o código alvo é código
de máquina então registros ou locais de memória são escolhidos para cada uma
das variáveis do programa e são escolhidas as operações equivalentes para as
instruções intermediárias criadas previamente. Esta é a tarefa mais complicada
para o gerador de código pois ele tem de escolher a instrução mais eficiente dentre
as possíveis (em máquinas CISC, por exemplo, o trabalho é ligeiramente mais
complicado pois há instruções com finalidades semelhantes (soma, incrementação)
dificultando assim a escolha), a escolha do local de armazenamento também é
complexa tendo em vista que há a possibilidade de se armazenar nos próprios
registradores na CPU, no topo da pilha
ou na memória geral. O acesso ao topo da pilha é mais rápido que na memória
geral, pois a memória na pilha é organizada de forma automática, o ponto fraco
do armazenamento na pilha é o tamanho de dados que é possível alocar, diferente
da memória, que tem um acesso mais lento, mas não possui restrições para uso
tanto quanto a pilha. Soluções heurísticas e ad hoc são utilizadas para resolver esses problemas.
Fonte:
Compilador, Wikipédia, a Enciclopédia Livre. Disponível na Internet via <http://pt.wikipedia.org/wiki/Compilador>
Aho, Alfred. V. et. al, Compilers: Principles, Techniques, and Tools, Second Edition. Addison-Wesley, 2007. ISBN 978-0-321-48681-3. Capítulos 1 e 2.
Aho, Alfred. V. et. al, Compilers: Principles, Techniques, and Tools, Second Edition. Addison-Wesley, 2007. ISBN 978-0-321-48681-3. Capítulos 1 e 2.
SANTOS, Ricardo. Compiladores I - Capítulo 1, 20-?. PDF. Disponível na Internet via <http://www.facom.ufms.br/~ricardo/Courses/CompilerI/Lectures/Lec01.pdf>
RANGEL, José Lucas. Geração de Código. PDF. Disponível na Internet via http://www-di.inf.puc-rio.br/~rangel/comp/gercod.pdf
Faculdade de Engenharia Elétrica e Computação - Disponível na Internet via http://www.fee.unicamp.br/?q=node/145
Gostou ? Deixa um comentário aqui embaixo, dúvidas e sugestões são bem vindas.
Nenhum comentário:
Postar um comentário