Aula 19 - Geração e Otimização de Código Intermediário ⚙️
Objetivo Pedagógico
Objetivo: Fases finais de compilação: Representação Intermediária (IR / Three-Address Code), otimizações de código (Constant Folding, Dead Code Elimination) e geração de Bytecode.
📑 1. Fundamentos Teóricos & Análise Técnica
Após a validação sintática e a análise semântica (verificação de tipos na tabela de símbolos), compiladores modernos não traduzem a AST diretamente para código de máquina. Traduzir \(N\) linguagens para \(M\) arquiteturas de processadores diretamente exigiria \(N \times M\) compiladores independentes.
A solução de engenharia universal (adotada pelo LLVM e GCC) consiste em converter a AST em uma Representação Intermediária (IR - Intermediate Representation), permitindo dividir o compilador em: - Frontend: Específico da linguagem (gera IR). - Optimizer: Otimiza a IR de forma independente do hardware. - Backend: Específico da arquitetura (converte IR para x86, ARM, RISC-V).
O formato de IR mais comum é o Código de Três Endereços (Three-Address Code - TAC), onde cada instrução possui no máximo um operador e três endereços (temp1 = a + b).
Técnicas clássicas de otimização de IR: 1. Constant Folding: Avaliação antecipada de operações constantes (2 + 3 vira 5). 2. Dead Code Elimination: Remoção de ramos condicionais ou variáveis que nunca são lidos no programa. 3. Loop Unrolling: Desdobramento de laços pequenos para eliminar o custo de saltos condicionais.
📐 Arquitetura Conceitual & Diagrama de Fluxo
flowchart TD
AST["AST Validada"] --> TACGen["Gerador de Três Endereços (TAC IR)"]
TACGen --> TACRaw["IR Não-Otimizada:<br>t1 = 10 * 2<br>t2 = t1 + x<br>if (false) { unreachable() }"]
TACRaw --> Optimizer["Módulo de Otimizações"]
Optimizer --> TACOpt["IR Otimizada:<br>t2 = 20 + x (Constant Folded & Dead Code Purged!)"]
TACOpt --> Backend["Backend: Emite Código de Máquina (x86/ARM/RISC-V)"]
style AST fill:#e1f5fe,stroke:#01579b
style TACGen fill:#fff3e0,stroke:#e65100
style Optimizer fill:#e8f5e9,stroke:#2e7d32
style Backend fill:#f3e5f5,stroke:#7b1fa2 🔍 Pilares e Diretrizes Técnicas
Nesta unidade, aprofundamos os seguintes conceitos fundamentais: - Agnosticismo de Arquitetura: As mesmas otimizações matemáticas funcionam para qualquer processador de destino. - Static Single Assignment (SSA): Propriedade em que cada variável da IR é atribuída exatamente uma vez, facilitando análises de fluxo de dados. - Grafos de Fluxo de Controle (CFG): Divisão do código em Blocos Básicos (Basic Blocks) conectados por arestas de salto. - Geração de Código Alvo: Alocação de registradores físicos via coloração de grafos (Register Allocation).
🛠️ 2. Implementação Prática em Compiladores, Otimização e Bytecode
Abaixo está a implementação técnica de referência, estruturada com padrões de engenharia de software e foco em robustez:
// intermediate_code.tac (Exemplo de Código de Três Endereços - TAC)
// Código Fonte Original:
// int total = (base * 2) + offset;
// Three-Address Code (TAC) gerado pelo Frontend:
t1 = 2
t2 = base * t1
t3 = t2 + offset
total = t3
// Otimização de Força (Strength Reduction):
// O backend substitui a multiplicação cara 'base * 2' por um deslocamento de bits barato:
t2 = base << 1
total = t2 + offset
💡 Análise Passo a Passo do Código
- Linearização da Árvore: A AST hierárquica é achatada em uma sequência plana de instruções elementares.
- Variáveis Temporárias t1, t2...: Registram os resultados parciais das subexpressões de forma atômica.
- Redução de Força: Otimização que troca instruções de clock alto por instruções binárias ultrarrápidas.
🎯 3. Próximos Passos & Sequência Didática
-
Slides da Aula
-
Quiz de Fixação
-
Exercícios Práticos
-
Desafio de Projeto