Trilha 12 · Linguagens e compiladores

JIT: compilação em tempo de execução

Por que V8 executa JavaScript quase tão rápido quanto C, enquanto Python interpretado é 50x mais lento — e como o JIT detecta e otimiza automaticamente os caminhos quentes.

① Intuição

Compilar o que é usado, quando é usado

Compilar todo o código antecipadamente (AOT) desperdiça tempo compilando código que raramente roda. Interpretar tudo é simples mas lento. O JIT (Just-In-Time compiler) faz o melhor dos dois: começa interpretando, monitora quais funções são chamadas frequentemente (hot paths), e compila só essas para código de máquina otimizado.

O JIT também faz especulação de tipos: se uma função sempre recebe inteiros, o JIT gera código que assume inteiros — eliminando as verificações de tipo que o interpretador faria a cada iteração. Se um float aparece, o JIT desotimiza e recompila.

Por que Python é lento? CPython não tem JIT (até o 3.13, que adiciona um JIT experimental). Cada operação a + b verifica o tipo de a e b em tempo de execução. PyPy, uma implementação alternativa de Python com JIT, é 5-10x mais rápida que CPython.
② Visualização interativa

Veja o JIT em ação

Clique em Executar loop. As primeiras iterações são interpretadas (barras maiores). Quando o JIT detecta o hot path, compila — e as barras ficam muito menores.

function somaQuadrados(n) {
  let soma = 0;
  for (let i = 1; i <= n; i++) {
    soma += i * i;          // ← caminho quente
  }
  return soma;
}
// chamada em loop:
for (let c = 0; c < 20; c++) somaQuadrados(1000);
TEMPO POR ITERAÇÃO (ms) — interpretando...
JIT compila →
it.0it.8 (JIT)it.19
INTERPRETADO
0.0ms
média por it.
Interpretar
iterações 1-7
Executa o AST/bytecode diretamente. Lento mas sem custo de compilação.
Detectar hot path
iterações 8
Contador de chamadas ≥ threshold. JIT engine começa a compilar o código quente.
Executar código nativo
iterações 9-20
Código compilado para máquina diretamente. Dezenas de vezes mais rápido.
③ Explicação técnica

Como o JIT decide o que compilar

// Como o JIT decide o que compilar

// Cada função tem um contador de chamadas
function chamada(fn) {
  fn.callCount++;

  if (fn.callCount === THRESHOLD_COMPILE) {
    // "Caminho quente" detectado! Compila para código nativo
    fn.nativeCode = jitCompile(fn.bytecode, fn.types);
  }

  if (fn.nativeCode) {
    return fn.nativeCode();    // execução nativa — 10-100x mais rápido
  } else {
    return interpretar(fn.bytecode);  // ainda interpretando
  }
}

// Deoptimização: se a suposição de tipo muda, joga fora
function soma(a, b) { return a + b; }
soma(1, 2);    // JIT assume: ambos são inteiros → gera ADD int32
soma(1.5, 2); // float aparece! Deoptimiza e recompila para float

O pipeline do V8

# Pipeline de execução do V8 (Chrome/Node.js)

JavaScript Source
      ↓
   [Parser]  → AST
      ↓
[Ignition]   → Bytecode (interpretador rápido)
      ↓ (hot functions)
[Sparkplug]  → Código nativo não otimizado (compilação rápida)
      ↓ (very hot)
[Maglev]     → Código nativo otimizado (médio prazo)
      ↓ (very very hot)
[TurboFan]   → Código nativo altamente otimizado (especulativo)
      ↓ (type changes)
  Deoptimize → volta ao Ignition bytecode

# V8 usa 4 níveis de otimização progressiva!
# Cada nível compila mais rápido mas gera código menos otimizado.
Inline caches: uma das otimizações mais impactantes do JIT. Na primeira chamada a obj.metodo(), o JIT verifica o tipo de obj e armazena na "cache" o ponteiro direto para o método. Na próxima chamada, pula a verificação de tipo — só compara "é o mesmo tipo?" e salta direto. Isso elimina o dispatch de método dinâmico, a operação mais cara de linguagens OOP.
④ Projeto para programar

Simule um JIT simples

Mini projeto: implemente um "mini JIT" simulado: mantenha um contador de chamadas por função. Quando o contador atinge 5, "compile" (no caso: gere um cache com os tipos dos parâmetros e retorne uma versão pré-calculada mais rápida). Teste com uma função chamada em loop.

Projeto principal: use a API performance.now() para medir o tempo de 1000 chamadas de uma função matemática pesada antes e depois de aplicar memoization (cache de resultados). Depois compare com uma versão em WebAssembly (compile o mesmo algoritmo em C para Wasm usando Emscripten).

Desafio extra: estude o bytecode do CPython e do V8 Ignition. Escreva uma função que é rápida no V8 (tipos estáveis, sem polimorfismo) e uma que força deoptimização (mistura de tipos no mesmo array). Meça com %NeverOptimizeFunction e --trace-opt do Node.js.

⑤ Exercícios rápidos

Teste sua intuição

O que diferencia JIT de AOT (compilação antecipada) e interpretação pura?
O JIT compila soma(a,b) assumindo que a é inteiro. Se a virar float, o que acontece?
Qual afirmação descreve corretamente o pipeline do V8?
⑥ Aplicações no mundo real

Onde você encontra isso

JavaScript no browser

V8 (Chrome/Node), SpiderMonkey (Firefox) e JavaScriptCore (Safari) todos têm JITs. É por isso que JavaScript moderno é rápido o suficiente para jogos 3D, editores de vídeo e CAD no browser — coisas que pareciam impossíveis em 2008.

🦀

WebAssembly vs. JS JIT

Wasm não precisa de JIT — é compilado AOT para bytecode tipado e verificável. O browser compila Wasm para código nativo em milissegundos. Para algoritmos numéricos, Wasm supera o JIT do JS porque não precisa especular sobre tipos.

🐍

PyPy e Python 3.13

PyPy é uma implementação de Python com JIT que chega a 10x mais rápido que CPython. Python 3.13 adicionou um JIT experimental (copy-and-patch). NumPy e numba usam JIT (via LLVM) para computação científica rápida em Python.

JVM HotSpot

A JVM HotSpot monitora bytecode e usa dois JITs: C1 (client, compila rápido) e C2 (server, compila lento mas gera código ótimo). Após warm-up (~10.000 chamadas), Java frequentemente supera C++ em benchmarks de longa duração.

← Anterior: Coleta de lixo ✓ Concluir trilha: Linguagens e compiladores