Escolar Documentos
Profissional Documentos
Cultura Documentos
1
Universidade Estadual do Centro-Oeste – UNICENTRO
Curso de Ciência da Computação
Professora Sandra Mara Guse Scós Venske
log n).
2. Melhorias
A fase de Otimização de código transforma o código intermediário numa forma a partir da qual se
obtém um código-alvo mais eficiente. O código produzido pelos algoritmos diretos de compilação
pode ser transformado de forma a executar mais rápido, a ocupar menos espaço ou ambas as
situações.
As otimizações podem ser dependentes ou independentes de máquina. As dependentes de
máquina trabalham com alocação de registradores e utilização de seqüências especiais de instruções
de máquina. As otimizações independentes de máquina referem-se a transformações de programa que
melhoram o código, sem observar as propriedades da máquina.
Alguns critérios a serem seguidos para as transformações de melhoria de código:
1. Uma transformação precisa preservar o significado dos programas, assim sendo, uma
otimização não pode modificar a saída para uma determinada entrada, ou causar um erro, que
já não estivesse presente no programa original. Aplica-se o chamado critério “seguro”,
optando por perder boas oportunidades, em vez de correr o risco de mudar o que o programa
faz.
2. Uma transformação precisa acelerar os programas por um fator mensurável. Por exemplo,
hoje, reduzir o espaço usado pelo código compilado tem menos importância do que tinha em
outras épocas. Deve-se ter em mente que a melhora pode ser “em média”, de tal forma que
pode retardar ligeiramente o programa em alguns pontos.
3. Uma transformação precisa valer o esforço. Somente quando o programa ocupa uma fração
significativa dos ciclos da máquina é que a qualidade do código melhorado justifica o tempo
gasto rodando-se um compilador otimizante para o mesmo.
Como mostra a Figura 1, as melhorias podem ser feitas em todos os níveis: a partir do
programa-fonte até o programa-alvo. As transformações que produzem maior resultado no tempo de
execução são as algorítmicas.
Nenhum compilador pode encontrar um melhor algoritmo para um dado programa; o que ele
pode fazer é substituir uma sequência de operações por outra equivalente algebricamente e, por
conseguinte, reduzir bastante o tempo de execução de um programa.
Por exemplo, considere um trecho de programa em que aparece um comando x=a+b; Este
2
Universidade Estadual do Centro-Oeste – UNICENTRO
Curso de Ciência da Computação
Professora Sandra Mara Guse Scós Venske
programa pode ser melhorado (torna-se mais rápido e menor) se este comando for
retirado. Mas para que o funcionamento do programa não seja alterado, é necessário mostrar uma
entre diversas propriedades, algumas das quais aparecem a seguir:
1. o comando x=a+b; nunca é executado. Por exemplo, está em seguida a um if cuja condição
nunca é satisfeita:
if(0)
x=a+b;
2. o comando é inútil, porque todas as vezes que é executado, x recebe exatamente o mesmo
valor que tinha antes . Por exemplo, pode-se retirar o segundo comando na sequência
x=a+b;
x=a+b;
3. o comando é inútil, porque nenhum dos comandos executados posteriormente, usa o valor da
variável x. Por exemplo, se aparecer no programa uma função
int f(int z) {
int x;
...
x=a+b;
}
o valor de x nunca será utilizado, simplesmente porque após a saída da função, a variável
x, que é local à função, não mais existe.
Este exemplo mostra que não seria prático tentar otimizar um programa tentando
sucessivamente eliminar todos os seus comandos, um de cada vez. Note que as condições para cada
eliminação dependem do comando, de sua posição, e, de certa maneira, de todos os comandos
restantes do programa. Para construir um otimizador de utilidade na prática, precisamos identificar
oportunidades para otimização que sejam produtivas em situações correntes. Por exemplo, nenhum
programador escreveria intencionalmente nenhum dos três trechos de código usados como exemplo
acima, de forma que não valeria a pena procurar exatamente estas situações.
Outro exemplo de otimização que é citado frequentemente é a retirada de comandos de um
comando de repetição (um loop). Por exemplo, se encontrarmos em um loop um comando cujo efeito
é independente do loop, pode valer a pena retirá-lo do loop, para que ele seja executado apenas uma
vez, em vez das muitas vezes que se presume que será executado o código de dentro do loop. Por
exemplo, se N tem o valor 100,
for (i=0; i<N; i++) {
a=j+5;
f(a*i);
}
poderia ser melhorado retirando-se o comando a=j+5; do for. Ficar-se-ia com
a=j+5;
for (i=0; i<N; i++)
f(a*i);
e 100 somas a menos seriam executadas. Por outro lado, se N=0, o programa foi “piorado”, porque o
comando a=j+5; que era executado 0 vezes, passou a ser executado 1 vez. Mas pode haver um
problema maior: se a variável a é usada após o loop, em vez de seu valor original, seu valor será,
incorretamente, o resultado dado pela atribuição.
Bloco básico
Para dividir um programa (ou um trecho de programa) em representação intermediária em blocos
básicos, temos o seguinte:
Primeiro comando inicia um bloco básico.
Qualquer comando rotulado, ou que de alguma forma seja alvo de um comando de desvio
inicia um novo bloco básico.
Qualquer comando de desvio, condicional ou incondicional termina um bloco básico.
Ou seja, blocos básicos (ou “trecho de código em linha reta”) são trechos de programa
3
Universidade Estadual do Centro-Oeste – UNICENTRO
Curso de Ciência da Computação
Professora Sandra Mara Guse Scós Venske
maximais cujas instruções são sempre executadas em ordem (em linha reta), da primeira
até a última. Maximal aqui quer dizer que é impossível acrescentar outra instrução mantendo a mesma
propriedade. Um bloco básico é uma sequência de enunciados consecutivos, na qual o controle entra
no início e o deixa no fim, sem uma parada ou salto, exceto no final. Várias técnicas de otimização se
aplicam a blocos básicos.
Propagação de Cópias
4
Universidade Estadual do Centro-Oeste – UNICENTRO
Curso de Ciência da Computação
Professora Sandra Mara Guse Scós Venske
Eliminação de Código-Morto
Supondo que um programa contenha código morto (dead code), isto é, código que não pode ser
alcançado durante a execução de um programa, este programa pode ser otimizado pela remoção deste
código. O código morto ser identificado em uma série de situações:
ocorre após uma instrução de encerramento de um programa ou de uma função. Por
exemplo, após um comando return;
int f(int x) {
return x++; /* o incremento não será executado */
}
ocorre após um teste com uma condição impossível de ser satisfeita. Por exemplo
#define DEBUG 0
...
if(DEBUG) {
... /* este código não será executado */
}
ocorre após um comando de desvio, e não é alvo de nenhum outro desvio.
goto x;
i=3; /* este código não será executado */
...
x: ...
Normalmente, não se espera que um programador escreva código morto. Entretanto, isso pode
acontecer em várias situações, uma das quais é um estágio intermediário da otimização de um
programa, que sofreu algumas transformações que causaram a existência de código não alcançável.
Otimização de Laços
Historicamente, o tratado fundamental sobre técnicas para otimização de loops foi publicado por
Frances Allen1, em 1969. Frances Allen foi ganhadora do ACM Turing Award de 2006, primeira
mulher em 40 anos de existência do prêmio, e realizou importantes trabalhos nos campos da
Otimização e Computação de Alto Desempenho.
Há várias otimizações que se aplicam a loops, a mais comum das quais é a transferência de
computações invariantes do loop para fora dele. (Suponha que o comando a ser movido para antes do
loop é x=e;.) Para que isto possa ser feito, é necessário verificar:
A expressão e é composta apenas de constantes, ou de variáveis cujos valores não são
alterados dentro do loop.
Nenhum uso de x, dentro ou fora do loop deve ter acesso a um valor de x diferente do
valor a que tinha acesso antes do original.
Em particular, o valor de x após a saída do loop deve ser o mesmo do programa original.
Mesmo assim, como visto anteriormente, é possível piorar alguns programas, quando o loop é
1
http://www.francesallen.com/
5
Universidade Estadual do Centro-Oeste – UNICENTRO
Curso de Ciência da Computação
Professora Sandra Mara Guse Scós Venske
executado zero vezes: o comando dentro do loop não seria executado, mas fora do loop
será sempre executado uma vez.
Um exemplo de aplicação desta otimização seria (supondo N>0, para simplificar):
for (i=0, i<N, i++) {
k=2*N;
f(k*i);
}
que se transformaria em
k=2*N;
for (i=0, i<N, i++)
f(k*i);
Movimentação de código
Diversas oportunidades de otimização estão associadas à análise de comandos iterativos. Uma
estratégia é a movimentação de código, aplicado quando um cálculo realizado dentro do laço na
verdade envolve valores invariantes na iteração. Por exemplo, a avaliação de limite-2 é um cômputo
laço - invariante no seguinte enunciado while:
while (i<=limite-2) /* o enunciado não muda limite */
Otimizações Algébricas
Podem-se aplicar algumas transformações baseadas em propriedades algébricas, como
comutatividade, associatividade, identidade, entre outras. As identidades aritméticas são:
x + 0 = 0 + x = x
x - 0 = x
x * 1 = 1 * x = x
x / 1 = x
As otimizações algébricas que substituem um operador mais dispendioso por um mais
econômico são conhecidas como redução de capacidade e são:
x ** 2 = x * x
2.0 * x = x + x
x / 2 = x * 0.5
As transposições (ou dobramento de constantes) para constantes usam expressões ou
subexpressões compostas de valores constantes podem ser avaliadas em tempo de compilação
(dobradas), evitando sua avaliação repetida em tempo de execução. Por exemplo, em
#define N 100
...
while (i<N-1) {
...
}
não há necessidade de calcular repetidamente o valor de N-1. Este valor pode ser pré-calculado, e
substituído por 99. Em alguns casos, problemas de portabilidade podem exigir que a expressão
constante seja avaliada na máquina em que a execução vai se realizar, porque a máquina em que a
compilação se realiza não é a máquina alvo. Neste caso em vez de avaliar a expressão em tempo de
compilação, ela é avaliada uma vez, logo no início da execução.
Fonte:
AHO, Alfred V.; SETHI, Ravi; ULLMAN, Jeffrey D. Compiladores: Princípios, Técnicas e
Ferramentas. São Paulo: Guanabara Koogan, 1995.