O que o compilador faz
O compilador não apenas traduz — ele transforma. Com -O2, ele elimina loops, inline funções, usa SIMD e gera código que seria difícil de escrever à mão. Veja a diferença.
Compilar ≠ traduzir literalmente
Um compilador sem otimização (-O0) traduz cada linha de C para uma sequência
de instruções assembly correspondente — variáveis locais na pilha, frames explícitos, loops
como estão escritos. O resultado é correto mas lento.
Com -O2 ou -O3, o compilador analisa todo o programa como um
grafo de dados e transforma agressivamente: elimina variáveis locais que podem virar
registradores, detecta padrões matemáticos (fórmulas fechadas para somas), substitui
branches por instruções condicionais sem desvio, e vetoriza loops com SIMD. O resultado
pode ser irreconhecível — mas semanticamente idêntico ao original.
Explorador de otimizações
Compare o assembly gerado com -O0 (sem otimização) e -O2 (otimizado) para diferentes funções C. Note como o compilador transforma completamente o código.
int add(int a, int b) {
return a + b;
}; -O0: sem otimização (cada var na pilha) add: push ebp mov ebp, esp sub esp, 8 ; espaço para a e b mov [ebp-4], edi ; salva 'a' na pilha mov [ebp-8], esi ; salva 'b' na pilha mov eax, [ebp-4] ; carrega 'a' add eax, [ebp-8] ; soma 'b' pop ebp ret
Dica: use gcc -O2 -S -o saida.s seu_codigo.c para ver o assembly que o GCC gera do seu código. Online: godbolt.org mostra assembly colorido em tempo real enquanto você digita C/C++/Rust.
Técnicas de otimização de compiladores
// As principais transformações que o compilador aplica // 1. CONSTANT FOLDING — avalia expressões constantes em compilação int x = 2 * 3 * 7; // → mov eax, 42 (sem multiplicação em runtime!) // 2. DEAD CODE ELIMINATION — remove código que nunca executa if (0) { printf("nunca"); } // → completamente removido // 3. LOOP UNROLLING — desdoblamento de loop // Original: for (i=0; i<4; i++) a[i] *= 2; // Após unroll: a[0] *= 2; a[1] *= 2; a[2] *= 2; a[3] *= 2; // Vantagem: menos branches, mais instruções independentes para ILP // 4. STRENGTH REDUCTION — troca op cara por op barata x * 4 // → shl eax, 2 (shift é mais rápido que MUL) x / 8 // → sar eax, 3 (shift aritmético) // 5. COMMON SUBEXPRESSION ELIMINATION (CSE) int a = x*x + 1; int b = x*x + 2; // x*x calculado só uma vez → reutilizado // 6. VECTORIZATION (auto-SIMD) // for (i=0; i<N; i++) c[i] = a[i] + b[i]; // → processa 8 floats por ciclo com AVX2 (ymm registers)
Quando o compilador NÃO pode otimizar
// Quando o compilador NÃO pode otimizar — volatile e barreiras // O compilador assume que variáveis locais só mudam pelo código que ele vê. // Para hardware/threads que mudam variáveis "por fora": volatile int* reg = (volatile int*)0xF000; *reg = 1; // NÃO pode ser removido como "escrita morta" *reg = 2; // NÃO pode ser combinado com o anterior // Barreira de memória — impede reordenação __asm__ volatile ("mfence" ::: "memory"); // Garante que todas as escritas anteriores são visíveis antes desta barreira // Por que isso importa? O compilador pode reordenar: x = 1; // pode ser movido depois de y = 2 se não houver dependência y = 2; // em outro thread, a ordem x=1 → y=2 pode não ser garantida! // → Use std::atomic ou mutex para sincronização correta entre threads
.c separadamente. Com LTO (-flto), o compilador salva o IR (intermediate
representation) nos objetos e otimiza o programa inteiro durante o link — permitindo inlining
cross-file, eliminação de funções mortas globais e propagação de constantes entre arquivos.
O Firefox usa LTO, ganhando 3–5% de velocidade de startup. Custo: link 2–3× mais lento.
Medindo o impacto das otimizações
Mini projeto: compile a mesma função de soma com -O0, -O1, -O2, -O3 e -Os (otimizar tamanho). Use time para medir o tempo de execução de N=100 milhões de iterações. Plote a speedup relativa a -O0. Qual nível dá o maior ganho? Qual a diferença entre -O2 e -O3?
Projeto principal: encontre um caso onde o compilador não consegue vetorizar automaticamente (aliasing de ponteiros) e adicione __restrict__ para indicar que os ponteiros não se sobrepõem. Compare o assembly antes e depois com -fopt-info-vec (GCC) que reporta quais loops foram vetorizados e por quê alguns não foram.
Desafio extra: use Profile-Guided Optimization (PGO): compile com -fprofile-generate, execute com dados reais, depois recompile com -fprofile-use. O compilador agora sabe quais branches são quentes (frequentes) e os otimiza agressivamente. Meça speedup em um parser JSON ou processador de texto com corpus real.
Teste sua intuição
volatile garante em C e o que ele não garante?
Onde você encontra isso
JavaScript JIT (V8, SpiderMonkey)
Engines modernas de JS compilam "hot" funções para código de máquina em runtime (JIT). V8 usa Maglev (JIT de média tier) e Turbofan (JIT de alta tier). Turbofan faz inline, type specialization (float64 vs. int32), range analysis e vetorização — transformações semelhantes às de um compilador AOT, mas com informações de tipo disponíveis apenas em runtime.
Compiladores de IA (XLA, TorchInductor)
TensorFlow usa XLA; PyTorch 2.x usa TorchInductor sobre LLVM/Triton. Ambos compilam grafos de operações de tensores para código nativo: fusão de kernels (elimina buffers intermediários), tiling para cache, geração de SIMD/CUDA. Um modelo de linguagem como o GPT-4 seria impossível de servir sem essas otimizações de compilador.
Auto-vetorização e álgebra linear
NumPy, BLAS (OpenBLAS, MKL) e libc funções como memcpy usam SIMD manualmente ou via auto-vetorização. Uma multiplicação de matrizes 1024×1024 bem otimizada em AVX-512 processa 512 bits (16 floats) por ciclo, por unidade de execução. A diferença entre código naïve e código vetorizado: 20–100×.