Linguagens de alto nível deveriam permitir ao programador
abstrair o algoritmo da sua implementação.
Para atingir esse objetivo, o programador não deveria ser
consciente da arquitetura em que seu programa irá rodar. Entretanto,
isso só é possível se o compilador possuir um bom otimizador;
de outra maneira, o programador precisa tomar cuidado para
seu programa não fique lento devido à má utilização dos
recursos que o processador oferece.
Os otimizadores que existem no mercado, entretanto, não
conseguem gerar o melhor código possível para um dado algoritmo.
E mesmo que conseguissem, não há como garantir que o código gerado
seja mesmo o melhor possível. Esse problema não existe com o
Super Otimizador mostrado nessa página: com ele, garantidamente
seu algoritmo é implementado da maneira ótima, sem nenhum espaço
para outras otimizações.
Para entender o funcionamento do Super Otimizador, precisamos
primeiro definir alguns conceitos:
Programa: Por programa entendemos uma seqüência de
operações que implementa uma função, ou seja, para uma dada
seqüência de dados de entrada, a saída gerada é sempre a mesma.
Programa equivalente: Dois programas são equivalentes
se, para uma mesma entrada, fornecem a mesma saída. Note que
a equivalência só ocorre se a propriedade anterior for válida para
todas as possíveis entradas do programa.
Compilador: A tarefa de um compilador é achar um
programa equivalente ao código fonte fornecido, e que rode
no processador alvo.
Métrica: Define-se uma métrica de um programa como
um valor que o caracteriza. Métricas possíveis são: o tamanho do
programa, em bytes; ou o número de ciclos que ele leva para
executar.
Comprimento: O comprimento de um programa assembly
é o número de instruções que ele possui. O comprimento é uma
métrica possível para um programa assembly.
Otimizador: A tarefa de um otimizador é, dado um código
fonte e um programa equivalente A, achar um segundo programa
equivalente B, tal que a métrica de B seja menor que a métrica de A.
Programa ótimo: O programa ótimo é o programa equivalente
a um código fonte que possui a menor métrica possível. Esse
deveria ser o objetivo máximo de um otimizador.
Métrica monotônica crescente: Uma métrica é monotônica
crescente se, ao adicionar uma instrução a um programa dado,
seu valor torna-se maior que antes, ou seja, a diferença entre
o valor novo e o anterior é positiva e maior que zero.
Tendo assimilado os conceitos acima, fica fácil entender o
funcionamento do Super Otimizador. Inicialmente, cria-se um conjunto
com todos os programas possíveis de comprimento unitário, e
testa-se cada um deles para checar se algum é equivalente ao
código fonte. Caso seja, anota-se a métrica desse programa e retira-se
do conjunto todos os programas de métrica maior que a obtida. Retira-se
também do conjunto todos os programas que não são equivalentes
ao fonte.
Prosseguindo, adiciona-se ao conjunto todos os programas possíveis
de comprimento dois, e novamente verifica-se se algum programa do
conjunto é equivalente ao fonte. Caso ache-se outro programa equivalente,
verifica-se se a métrica dele é menor que a métrica já obtida. Caso seja,
troca-se a métrica alvo e retira-se do conjunto todos os programas
de métrica maior que a obtida. Continua-se esse algoritmo até
que haja pelo menos um programa equivalente encontrado, e todos
os programas do comprimento atual tenham métrica maior que o alvo atual.
Esse método gera todos os programas possíveis para a arquitetura
dada, ou seja, garantidamente ele vai achar pelo menos um programa
que implementa o fonte fornecido. Além disso, como a métrica é
monotônica crescente, então é certo que a partir de um certo ponto
todos os programas gerados terão métrica maior que a métrica do
programa equivalente encontrado. E, por fim, como todos os programas
possíveis menores que a dada métrica foram testados, então certamente
o programa equivalente encontrado é ótimo.
A implementação do Super Otimizador para a arquitetura Z80
foi feita com base em emulação. Para testar se um programa é
equivalente ao fonte fornecido, o fonte é compilado, e o programa
a ser testado é emulado e comparado com o binário para
todas as suas entradas possíveis. Como o fonte é testado
após a compilação, a linguagem de entrada é irrelevante,
sendo necessário apenas o binário do programa a ser testado.
Nesta implementação optou-se por limitar o programa à linguagem
ANSI C, mas isso pode ser facilmente estendido.
Para rodar o Super Otimizador, basta compilar o programa
opt.c e preencher os parâmetros corretos no início do arquivo.
As opções possíveis são:
MAXLEVEL: Este símbolo determina o comprimento
máximo dos programas a serem inspecionados. Um valor MAXLEVEL=2 indica
que todos os programas de comprimento menor ou igual a 3 serão pesquisados.
Atualmente o limite para MAXLEVEL é 7.
CARRYSOL: Caso definido, esse símbolo indica que
o resultado do programa deve ser retornado no flag de carry.
Caso não definido, o resultado estará no acumulador.
measure_type: Essa váriavel indica qual a métrica a
ser utilizada. Valores possíveis são: 0=métrica de comprimento,
1=métrica de bytes utilizados (otimização por espaço), 2=métrica
de ciclos de execução (otimização por velocidade, nesse caso
é utilizada a tabela de ciclos de um Z80 com um waitstate no
ciclo M1).
ARGS: Este símbolo determina o número de parâmetros
de entrada do programa. Para ARGS=1, você deve preencher a função
final
com o programa a ser compilado, o parâmetro de
entrada é input
. Para ARGS=2, você deve preencher
final2
, e os parâmetros de entrada serão a
e h
.
O Super Otimizador é exponencial no comprimento do programa
encontrado, o que significa que é bastante lento. Para encontrar
uma solução de comprimento seis ele leva 12 horas num Athlon 1.4GHz,
embora seja bem mais rápido para comprimentos menores (um programa de
comprimento 3 leva menos de 15 segundos).
Estes tempos só foram conseguidos utilizando otimizações na
árvore de busca. Em especial, caso tenha sido encontrado
um programa equivalente, programas com métrica maior que ele
nem chegam a ser testados.
Além disso, uma análise de fluxo de dados impede que programas
de registros indefinidos sejam testados. Assim, um programa do
tipo ADD A,H
não é testado pois ele sabe que o
valor do registro H não está determinado, porém LD H,A; ADD A,H
é testado pois o valor de H está determinado.
Se você tiver tempo disponível de CPU, abaixo estão
algumas funções cujo programa ótimo eu gostaria de ver.
(Dica: deixe rodando à noite, enquanto você estiver dormindo).
Mande seus resultados para ricardo@700km.com.br, e adicione-os
a esta página para referência futura.
return a&128?h:0; /* passo de multiplicação */
return a&1?h:0; /* passo de multiplicação */
return a>h?a:h; /* função max */
return (signed char)a<(signed char)h; /* comparação com sinal */