O melhor livro para responder sua pergunta provavelmente seria: Cooper e Torczon, "Engineering a Compiler", 2003. Se você tiver acesso a uma biblioteca universitária, poderá emprestar uma cópia.
Em um compilador de produção como llvm ou gcc, os designers fazem todos os esforços para manter todos os algoritmos abaixo de que é o tamanho da entrada. Para algumas análises para as fases de "otimização", isso significa que você precisa usar heurísticas em vez de produzir um código realmente ideal.O(n2)n
O lexer é uma máquina de estados finitos, portanto no tamanho da entrada (em caracteres) e produz um fluxo de tokens que são passados para o analisador.O(n)O(n)
Para muitos compiladores para vários idiomas, o analisador é LALR (1) e, portanto, processa o fluxo de token no tempo no número de tokens de entrada. Durante a análise, você normalmente precisa acompanhar uma tabela de símbolos, mas, para muitos idiomas, isso pode ser tratado com uma pilha de tabelas de hash ("dicionários"). Cada acesso ao dicionário é , mas você pode ocasionalmente ter que percorrer a pilha para procurar um símbolo. A profundidade da pilha é onde é a profundidade de aninhamento dos escopos. (Então, em idiomas do tipo C, quantas camadas de chaves você possui.)O(n)O(1)O(s)s
Em seguida, a árvore de análise é normalmente "achatada" em um gráfico de fluxo de controle. Os nós do gráfico de fluxo de controle podem ser instruções com três endereços (semelhante a uma linguagem de montagem RISC), e o tamanho do gráfico de fluxo de controle normalmente será linear no tamanho da árvore de análise.
Em seguida, uma série de etapas de eliminação de redundância é normalmente aplicada (eliminação de subexpressão comum, movimento invariante do código do loop, propagação constante, ...). (Isso geralmente é chamado de "otimização", embora raramente haja algo ideal sobre o resultado, o objetivo real é melhorar o código o máximo possível dentro das restrições de tempo e espaço que colocamos no compilador.) Cada etapa de eliminação de redundância será normalmente exigem provas de alguns fatos sobre o gráfico de fluxo de controle. Essas provas geralmente são feitas usando a análise de fluxo de dados . A maioria das análises de fluxo de dados é projetada para convergir em passa sobre o gráfico de fluxo, onde é (grosso modo) a profundidade de aninhamento do loop e uma passagem sobre o gráfico de fluxo leva tempoO(d)dO(n)onde é o número de instruções com três endereços.n
Para otimizações mais sofisticadas, convém fazer análises mais sofisticadas. Nesse ponto, você começa a encontrar trocas. Você deseja que seus algoritmos de análise tomem muito menos queO(n2)tempo no tamanho do gráfico de fluxo do programa inteiro, mas isso significa que você precisa ficar sem informações (e com o programa melhorando as transformações) que podem ser caras de provar. Um exemplo clássico disso é a análise de alias, em que, para alguns pares de gravações de memória, você gostaria de provar que as duas gravações nunca podem atingir o mesmo local de memória. (Você pode fazer uma análise de alias para ver se é possível mover uma instrução acima da outra.) Mas, para obter informações precisas sobre alias, pode ser necessário analisar todos os caminhos de controle possíveis no programa, o que é exponencial no número de ramificações no programa (e, portanto, exponencial no número de nós no gráfico de fluxo de controle.)
Em seguida, você entra na alocação de registros. A alocação de registro pode ser formulada como um problema de coloração de gráfico, e sabe-se que a coloração de um gráfico com um número mínimo de cores é NP-Hard. Portanto, a maioria dos compiladores usa algum tipo de heurística gananciosa combinada com derramamento de registro, com o objetivo de reduzir o número de derramamentos de registro da melhor forma possível, dentro de um prazo razoável.
Finalmente, você entra na geração de código. A geração de código geralmente é realizada como um bloco básico máximo no momento em que um bloco básico é um conjunto de nós de gráfico de fluxo de controle conectados linearmente com uma única entrada e uma única saída. Isso pode ser reformulado como um problema de cobertura de gráfico, onde o gráfico que você está tentando cobrir é o gráfico de dependência do conjunto de instruções de 3 endereços no bloco básico e você está tentando cobrir com um conjunto de gráficos que representam a máquina disponível instruções. Esse problema é exponencial no tamanho do maior bloco básico (que poderia, em princípio, ser da mesma ordem que o tamanho de todo o programa); portanto, isso normalmente é feito com heurísticas, onde apenas um pequeno subconjunto das coberturas possíveis é examinado.