Pular para conteúdo

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

  1. Linearização da Árvore: A AST hierárquica é achatada em uma sequência plana de instruções elementares.
  2. Variáveis Temporárias t1, t2...: Registram os resultados parciais das subexpressões de forma atômica.
  3. 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