ADRIANOLAUREANO← Artigos
MOTOR JAVASCRIPTPROFILING + JITSHAPES + CACHERUST

Como um motor JavaScript fica rápido?
Profiling, shapes, inline caches e JIT

Nosso motor já entende JavaScript, executa bytecode e mantém uma página viva. Mas ele ainda repete o caminho mais genérico mesmo quando uma função recebe objetos parecidos milhares de vezes. Neste capítulo construiremos o Janela JIT 0.5: um motor que observa o programa, encontra trabalho repetido, cria atalhos seguros e sabe abandoná-los quando suas suposições deixam de valer.

ARTIGO 20 · SÉRIE 05 · O MOTOR QUE APRENDE

31etapas didáticas

3caminhos de execução

1JIT em Rust

DO BYTECODE AO ATALHO · PROFILER, SHAPES, CACHE, JIT E DESOTIMIZAÇÃO
Ilustração técnica colorida mostrando bytecode, perfil de calor, formas de objetos, cache, compilador JIT, blocos otimizados e uma rota segura de desotimização

VOCABULÁRIO VISUAL

Antes de avançar, conheça as peças deste capítulo.

01Profilingmedição do programa enquanto ele roda
02Hot pathcaminho executado muitas vezes
03Shapedescrição da estrutura de um objeto
04Inline cachememória de shape e slot num ponto do código
05JITcompilador que trabalha durante a execução
06Desotimizaçãoretorno seguro ao caminho geral
Você não precisa memorizar agora. Este mapa existe para consultar sempre que um termo reaparecer.
01 /
Nosso Motor Funciona, mas Ainda Não Aprende com a Repetição
Correto vem antes de rápido.

No artigo “Como o navegador entende e executa JavaScript”, construímos lexer, parser, bytecode e uma máquina virtual. No artigo “Como o runtime mantém uma página viva”, acrescentamos memória, tarefas, Promises e event loop. Agora observaremos uma função simples:

function somaPonto(ponto) {
    return ponto.x + ponto.y;
}

for (let i = 0; i < 100_000; i++) {
    somaPonto({ x: i, y: 2 });
}

A primeira chamada precisa descobrir o que é ponto, localizar x, localizar y, conferir os tipos e somar. A centésima milésima recebe objetos criados do mesmo jeito. Um motor rápido aproveita esse padrão sem alterar o significado do programa.

02 /
V8 é um Motor; JavaScript é a Linguagem
A especificação define o comportamento, o motor escolhe como realizá-lo.

V8 é um motor JavaScript de código aberto usado no Chrome e em outros ambientes. Ele é uma implementação da linguagem, não a própria linguagem. Outros motores podem executar o mesmo programa usando arquiteturas internas diferentes.

O que construiremos

“Nossa V8” é uma expressão didática. Não copiaremos milhões de linhas nem prometeremos desempenho de produção. Isolaremos ideias centrais de motores modernos para enxergar por que elas existem e como cooperam.

03 /
Interpretar é Executar uma Instrução por Vez
A máquina virtual lê bytecode e decide o que cada operação significa.

Um interpretador percorre instruções e executa uma rotina para cada uma. O bytecode é o formato compacto que criamos no artigo “Como o navegador entende e executa JavaScript”. Para ponto.x + ponto.y, usaremos:

LoadArgument
LoadField x
LoadArgument
LoadField y
Add
Return

Essa abordagem começa rápido porque não precisa preparar código nativo antes da primeira execução. O custo aparece quando a mesma sequência genérica é repetida muitas vezes.

04 /
Código Nativo é a Forma que a CPU Executa Diretamente
Bytecode pertence à nossa VM; instruções de máquina pertencem ao processador.

Código nativo é composto por instruções da arquitetura real, como x86-64 ou ARM64. Já seguimos esse caminho em Do LLVM ao executável. Aqui não emitiremos bytes reais: criaremos uma representação Rust especializada que possui a mesma decisão importante — remover trabalho genérico quando as condições conhecidas continuam verdadeiras.

FormaQuem executaVantagem didática
JavaScriptmotorlegível pelo autor
Bytecodemáquina virtualcompacto e portátil
Caminho especializadonosso JIT em Rustmenos decisões repetidas
Código nativo realCPUmáximo aproveitamento do hardware
05 /
JIT Compila Durante a Execução
Just in time significa “bem na hora”.

JIT, abreviação de just-in-time compiler, é um compilador que trabalha enquanto o programa está rodando. Em vez de compilar tudo antecipadamente, ele pode esperar para descobrir quais partes merecem investimento.

IR é a forma intermediária usada pelo compilador

IR vem de intermediate representation, ou representação intermediária. Ela fica entre o bytecode de entrada e o código especializado de saída. Em vez de otimizar diretamente uma sequência de bytes, o compilador monta operações e relações mais convenientes para análise.

Bytecode: LoadField x · LoadField y · Add
IR:      GuardShape S2 · LoadSlot 0 · LoadSlot 1 · AddNumber
Saída:   caminho especializado protegido

No Janela JIT, OptimizedFunction é uma IR mínima: registra o shape esperado e os slots já resolvidos. Não emitimos instruções executáveis reais; conservamos a decisão importante e observável do JIT.

DUAS ROTASComeçar simples, otimizar o que se repete
01Bytecodepronto rapidamente
02Interpretadorcoleta observações
03JITespecializa trecho quente
04Atalhomenos verificações
06 /
Compilar Também Custa Tempo e Memória
O melhor atalho não é gratuito.

O JIT precisa analisar instruções, escolher especializações e guardar o resultado. Se uma função roda uma única vez, esse esforço talvez custe mais do que interpretar. Por isso um motor não deveria otimizar cegamente tudo.

A troca

Gastamos um pouco de tempo agora para economizar mais tempo depois. A otimização só compensa quando o trecho continua sendo usado.

07 /
Profiling é Medir o Programa Enquanto Ele Roda
Antes de otimizar, precisamos saber onde o tempo se repete.

Profiling é a coleta de medidas sobre a execução: quantidade de chamadas, tipos vistos, caminhos percorridos e tempo consumido. Um profiler é o componente que registra essas medidas.

pub struct Profiler {
    calls: HashMap<usize, u64>,
}

pub fn record_call(&mut self, function: usize) -> u64 {
    let count = self.calls.entry(function).or_default();
    *count += 1;
    *count
}

O identificador function distingue funções. O contador diz quantas vezes cada uma começou. Não mede tudo que um profiler profissional mede, mas responde à primeira pergunta necessária.

08 /
Hot Path é um Caminho Executado Muitas Vezes
“Quente” é uma metáfora para frequência, não temperatura.

Um hot path, ou caminho quente, é uma região do programa que concentra muita execução. Pode ser uma função inteira, um laço ou um ramo específico de uma condição.

No Janela JIT, a função fica quente ao atingir um limite:

let calls = profiler.record_call(function.id);

if calls >= hot_threshold {
    try_compile(function.id, argument);
}

Usaremos um limite pequeno nos exemplos para observar a mudança. Motores reais ajustam decisões com muito mais sinais.

09 /
Warm-up é o Período em que o Motor Aprende
As primeiras execuções ainda percorrem o caminho genérico.

Warm-up, ou aquecimento, é o intervalo inicial em que o interpretador executa e reúne observações. Um benchmark que mede apenas a primeira chamada pode capturar compilação, inicialização e caches frios em vez do estado estável.

ChamadaCaminhoO que o motor aprende
1VMshape e tipos vistos
2VMo padrão se repetiu
3VM + compilaçãofunção cruzou o limite
4JITguard confirma o padrão
10 /
Objetos JavaScript Podem Mudar de Estrutura
Flexibilidade para o autor cria trabalho para o motor.
const ponto = {};
ponto.x = 10;
ponto.y = 20;
ponto.cor = "laranja";

Campos podem ser acrescentados em tempo de execução. Se guardássemos tudo apenas num mapa de nomes, cada leitura precisaria procurar a string "x". Precisamos de uma forma de reconhecer objetos construídos pelo mesmo caminho.

11 /
Shape Descreve a Estrutura de um Objeto
Objetos com os mesmos campos na mesma ordem podem compartilhar um mapa.

Shape significa forma. É uma descrição interna da disposição dos campos, não o objeto nem seus valores. Dois pontos podem compartilhar o shape [x, y] mesmo contendo números diferentes. Na V8, a estrutura equivalente costuma ser chamada de Map ou HiddenClass; usamos “shape” porque descreve a ideia sem fingir que nossa estrutura possui todos os metadados da V8. A documentação oficial sobre propriedades rápidas na V8 mostra essa relação.

pub type ShapeId = usize;

pub struct Shape {
    fields: Vec<String>,
}

pub struct Object {
    shape: ShapeId,
    slots: Vec<Value>,
}

Slot é uma posição numerada. No shape [x, y], x ocupa o slot 0 e y o slot 1. Localizar um número conhecido é mais direto que pesquisar uma string repetidamente.

12 /
Adicionar um Campo Cria uma Transição de Shape
A sequência de construção forma um caminho reutilizável.
Shape 0: []
   + x → Shape 1: [x]
   + y → Shape 2: [x, y]

Uma transição liga o shape atual ao shape resultante da adição de um campo. Se milhares de objetos recebem x e depois y, todos reutilizam os mesmos identificadores.

pub fn transition(&mut self, shape: ShapeId, name: &str) -> ShapeId {
    if let Some(id) = self.transitions.get(&(shape, name.to_owned())) {
        return *id;
    }
    // cria a nova forma apenas na primeira vez
}
13 /
A Ordem de Criação Pode Produzir Shapes Diferentes
Os valores parecem iguais para o programa, mas chegaram por caminhos distintos.
const a = {}; a.x = 1; a.y = 2; // [x, y]
const b = {}; b.y = 2; b.x = 1; // [y, x]

Ambos possuem x e y, mas seus slots não coincidem. Isso não muda o resultado do JavaScript; apenas influencia as oportunidades internas de reutilização.

14 /
Um Site de Acesso é um Ponto Específico do Código
Duas leituras de x em linhas diferentes mantêm históricos separados.

Chamaremos de site o local de uma operação no bytecode. Cada LoadField recebe um número:

LoadField { site: 0, name: "x" }
LoadField { site: 1, name: "y" }

O site 0 aprende sobre objetos vistos ao ler x. O site 1 aprende independentemente ao ler y.

15 /
Inline Cache Lembra a Resposta do Último Padrão
Se o shape se repete, o slot já é conhecido.

Cache é uma memória de resultados úteis para evitar trabalho repetido. Um inline cache fica associado a um site de operação e guarda algo como: “quando o shape é 2, o campo x está no slot 0”.

pub struct CacheEntry {
    pub shape: ShapeId,
    pub slot: usize,
}

O nome “inline” vem da maneira como essa verificação pode ser colocada perto da própria operação em implementações reais. Nosso projeto conserva as entradas numa tabela para tornar o estado fácil de inspecionar.

16 /
Cache Hit Reutiliza; Cache Miss Precisa Descobrir
Acerto economiza busca, falha atualiza o conhecimento.

Um cache hit, ou acerto, acontece quando o shape recebido é o esperado. Um cache miss, ou falha, acontece quando não existe entrada ou o shape mudou.

if entry.shape == object.shape() {
    self.hits += 1;
    return object.slot(entry.slot);
}

self.misses += 1;
let slot = shapes.slot(object.shape(), name)?;

Miss não significa erro do programa. O motor segue o caminho geral, encontra a resposta correta e pode atualizar o cache.

17 /
Monomórfico, Polimórfico e Megamórfico Medem a Variedade
Um site pode ver uma, algumas ou muitas formas.
EstadoO que o site observouEstratégia comum
Monomórficoum shape recorrenteum teste e um slot
Polimórficopoucos shapespequena lista de casos
Megamórficomuitos shapescaminho mais geral

Mono significa um; poli, vários. “Megamórfico” indica variedade tão grande que acrescentar mais casos especializados pode deixar de compensar. Nosso cache é monomórfico para manter o mecanismo visível.

18 /
Especialização Troca Generalidade por um Caminho Mais Curto
O JIT usa as observações do interpretador.

Especializar significa preparar código para um caso observado. Se somaPonto sempre recebe o shape [x, y] com números, podemos buscar diretamente os slots e somá-los.

struct OptimizedFunction {
    expected_shape: ShapeId,
    x_slot: usize,
    y_slot: usize,
}

Essa estrutura é o nosso “código compilado”. Em um motor profissional, a saída poderia ser instrução de máquina numa região executável de memória.

19 /
Guard Verifica se a Suposição Continua Verdadeira
O atalho só é correto dentro das condições que o criaram.

Guard significa guarda ou verificação de proteção. Antes de usar os slots compilados, conferimos o shape:

if object.shape() != compiled.expected_shape {
    return None;
}

Sem esse teste, um objeto [y, x] faria o motor confundir posições. Otimização não autoriza mudar o comportamento da linguagem.

20 /
O Caminho JIT Remove Decisões que Já Foram Resolvidas
Shape esperado, slots conhecidos e tipos numéricos.
let x = object.slot(compiled.x_slot)?.as_number()?;
let y = object.slot(compiled.y_slot)?.as_number()?;

Some(Value::Number(x + y))

Não percorremos seis instruções, não pesquisamos nomes e não despachamos a operação Add genérica. O trabalho economizado é pequeno numa chamada e significativo quando multiplicado por muitas repetições.

21 /
Desotimização Volta ao Caminho Geral
Uma suposição quebrada não pode quebrar o programa.

Desotimização, ou deoptimization, acontece quando o código especializado encontra algo que não previu. O guard falha e o motor retorna ao interpretador. Alguns textos chamam a saída rápida do caminho otimizado de bailout, “abandono do atalho”; neste artigo usaremos desotimização para nomear o processo completo.

match run_optimized(compiled, argument) {
    Some(value) => value,
    None => {
        deoptimizations += 1;
        optimized.remove(&function.id);
        interpret(function, argument)?
    }
}

Nosso projeto reinicia a função na VM. Motores profissionais conseguem reconstruir frames e continuar em pontos mais precisos, mas a regra central é a mesma: preservar o resultado correto.

22 /
Uma Pequena Mudança Pode Invalidar o Atalho
O motor reage ao que aconteceu, não ao que desejávamos que acontecesse.
// padrão que aqueceu a função
{ x: 3, y: 4 }

// novo caminho de construção
{ tag: 0, x: 3, y: 4 }

O segundo objeto continua válido e a soma deve continuar devolvendo 7. Seu shape possui outro arranjo. O guard detecta a diferença, registra uma desotimização e entrega o trabalho à VM.

23 /
Agora Podemos Ver o Pipeline Inteiro
Cada peça responde a uma pergunta diferente.
DO FONTE AO ATALHOO motor alterna aprendizagem e especialização
01VMexecuta corretamente
02Profiler + cacheobservam repetição
03JIT + guardcriam atalho seguro
04Deoptvolta se mudar
PeçaPergunta
Profilero que se repete?
Shapecomo este objeto está organizado?
Inline cacheposso reutilizar o slot anterior?
JITvale criar um caminho especializado?
Guarda hipótese ainda vale?
Deoptcomo volto à segurança?
24 /
Engine Reúne Observação, Cache e Compilação
O estado da otimização pertence ao motor, não à função JavaScript.
pub struct Engine {
    pub shapes: ShapeTable,
    profiler: Profiler,
    caches: InlineCaches,
    optimized: HashMap<usize, OptimizedFunction>,
    hot_threshold: u64,
    deoptimizations: u64,
}

ShapeTable compartilha formas, Profiler conta chamadas, InlineCaches lembram slots e optimized guarda funções já especializadas.

25 /
run Escolhe VM, JIT ou Retorno Seguro
A decisão ocorre em cada chamada observada.
pub fn run(&mut self, function: &Function, argument: Value)
    -> Result<ExecutionReport, String>
{
    let calls = self.profiler.record_call(function.id);

    // usa JIT quando disponível;
    // desotimiza se o guard falhar;
    // interpreta enquanto a função aquece.
}

O ExecutionReport expõe caminho, chamadas, hits, misses e desotimizações. Essa observabilidade permite aprender e testar o mecanismo sem cronômetros instáveis.

26 /
Testes Comprovam Mudança de Caminho e Correção
Desempenho não serve se o resultado deixar de ser confiável.
assert_eq!(first.path, "VM");
assert_eq!(second.path, "VM");
assert_eq!(third.path, "JIT");
assert_eq!(third.value.as_number(), Some(9.0));

O segundo teste aquece o JIT com [x, y], envia [tag, x, y] e espera:

assert_eq!(report.path, "DESOTIMIZAÇÃO → VM");
assert_eq!(report.value.as_number(), Some(7.0));
assert_eq!(report.deoptimizations, 1);
27 /
Benchmark Precisa Separar Aprendizagem de Estado Estável
Uma medição honesta declara o que está incluindo.

Benchmark é um experimento que mede desempenho sob regras reproduzíveis. Para motores com JIT, precisamos decidir se queremos medir inicialização, warm-up, execução já otimizada ou desotimização.

  • Repita o cenário e descarte ou relate o aquecimento.
  • Evite misturar criação de dados com o trecho que deseja medir.
  • Confira o resultado para impedir que trabalho inútil seja removido.
  • Compare distribuições e não apenas um número isolado.

Nosso projeto mede contagens e caminhos, não nanossegundos. Isso torna a conclusão determinística: sabemos exatamente quando a política mudou.

28 /
O Janela JIT 0.5 Junta Tudo num Projeto Executável
Cada módulo corresponde a um conceito explicado.
janela-jit/
├── Cargo.toml
├── README.md
├── src/
│   ├── bytecode.rs       # instruções da VM
│   ├── object.rs         # shapes, transições e slots
│   ├── inline_cache.rs   # hits, misses e entradas
│   ├── profiler.rs       # contador de chamadas
│   ├── engine.rs         # VM, JIT, guards e deopt
│   ├── value.rs          # números e objetos
│   ├── lib.rs
│   └── main.rs
└── tests/engine.rs

Execute cargo run para observar a função migrando da VM ao JIT. Execute cargo test para validar tanto o caminho quente quanto a desotimização.

29 /
Um Motor Profissional Possui Muito Mais Camadas
Nosso modelo preserva as decisões, não a escala industrial.
ÁreaJanela JIT 0.5Motor profissional
Profilerconta chamadastipos, ramos, laços e amostragem
Cachemonomórficomono, poli e megamórfico
JITfunção especializada em Rustmúltiplos níveis e código nativo
Otimizaçõesslots e soma numéricainlining, escape analysis, folding e mais
Deoptreinicia na VMreconstrói estado e continua
Memóriaobjetos com RcGC geracional, incremental e concorrente

Tier significa nível de execução. Um motor moderno pode começar no interpretador, passar por um compilador rápido e investir num compilador mais caro somente nos trechos mais importantes. Na V8 atual, Ignition interpreta bytecode; Sparkplug gera código rapidamente; Maglev ocupa um nível otimizador intermediário; TurboFan busca desempenho de pico. O artigo oficial sobre o compilador Maglev explica por que esses níveis equilibram início rápido e velocidade sustentada.

Inlining substitui uma chamada pelo corpo da função. Escape analysis procura valores que não escapam de um escopo. Constant folding calcula expressões constantes antecipadamente. Esses termos aparecem aqui apenas para dimensionar o campo, não como pré-requisitos escondidos.

30 /
Mapa de Consulta Rápida
Uma frase recupera cada conceito do capítulo.
TermoSignificado
Interpretadorexecuta bytecode instrução por instrução
Profilingmedição do programa enquanto roda
Hot pathcaminho executado muitas vezes
Warm-upperíodo inicial de aprendizagem
Shapedescrição da estrutura de um objeto
Slotposição numerada de um campo
Inline cachememória do shape e slot vistos num site
JITcompilador que trabalha durante a execução
Guardteste que protege uma hipótese otimizada
Desotimizaçãoretorno ao caminho geral quando a hipótese falha
31 /
O Motor Ficou Mais Rápido sem Deixar de Ser Correto
Observar, especializar, verificar e recuar.

Começamos com bytecode interpretado. O profiler encontrou repetição. Shapes deram identidade estrutural aos objetos. Inline caches transformaram nomes recorrentes em slots. O JIT criou um caminho especializado. Guards protegeram suas suposições. Quando o mundo mudou, a desotimização devolveu a execução à VM.

A REGRA FINALVelocidade nasce de hipóteses verificáveis
01Observarsem adivinhar
02Especializarno caso frequente
03Protegercom guards
04Recuarsem perder correção
A série completa

Partimos de uma janela desenhada em pixels, abrimos um site, entendemos JavaScript, mantivemos a página viva e, por fim, ensinamos nosso motor a reconhecer padrões de execução. Cada camada continuou ligada às anteriores — da interface ao código otimizado.

PROJETO COMPLETO · CÓDIGO DO CAPÍTULO

Execute e investigue o Janela JIT 0.5.

O pacote contém bytecode, interpretador, profiler, object shapes, transições, slots, inline caches com hits e misses, compilação de uma função quente, guards, desotimização, relatório observável e testes que comprovam correção nos três caminhos.

Baixar código final deste artigo (.zip)Rust · profiling · shapes · inline cache · JIT · deopt

O que este artigo fez você pensar?

Dúvidas, experiências e contrapontos ajudam a próxima pessoa a enxergar o assunto por outro ângulo.

Todos passam por moderação. Ao enviar, você concorda com a política de privacidade.

Receba os próximos artigos.

Uma mensagem quando uma nova investigação estiver pronta. Só isso.