Artigos aprofundados sobre a tecnologia que molda o que vem a seguir.

O JIT Compiler do Python Finalmente Está Chegando

Python 3.15 traz um JIT compiler copy-and-patch em desenvolvimento há anos. Veja como funciona, que ganhos esperar e por que o CPython demorou tanto.

Cobra equipada com uma mochila a jato de trilhas de circuito, pronta para disparar por uma pista

O Python é 'lento' desde que o Python existe. A resposta padrão da comunidade, 'use extensões em C para os loops críticos', sempre foi uma concessão de que o modelo de execução padrão da linguagem é fundamentalmente limitado. O CPython interpreta bytecode uma instrução por vez, com cada instrução despachada por meio de um switch statement. É simples, portátil e fácil de depurar. Também é cerca de 100x mais lento que C compilado em trabalhos com muito processamento.

O Python 3.15 muda isso. Depois de anos de trabalho experimental, o JIT compiler copy-and-patch chega como um recurso habilitado por padrão. Ele não vai deixar o Python tão rápido quanto C (nada vai, a não ser a compilação estática), mas os primeiros benchmarks mostram ganhos de 15 a 30% em código do mundo real, com alguns padrões específicos tendo melhorias bem maiores. Para uma linguagem onde 'basta reescrever o trecho crítico em C' é a história de performance há 30 anos, um JIT que acelera de forma significativa código Python puro é um marco de verdade.

Por que o CPython Nunca Teve um JIT

Não é porque ninguém tentou. O PyPy tem um JIT há mais de uma década e costuma rodar código Python de 5 a 10x mais rápido que o CPython. Mas o PyPy é uma implementação separada, com runtime próprio, e nunca conquistou a fatia de mercado do CPython, porque o ecossistema de extensões em C (NumPy, pandas, scikit-learn, tudo o que faz do Python a linguagem da ciência de dados) está amarrado à C API do CPython.

Colocar um JIT dentro do próprio CPython já foi discutido e tentado várias vezes. Os desafios são bem documentados. A arquitetura do CPython torna a compilação JIT difícil: o bytecode é tipado dinamicamente (o JIT precisa de informação de tipos para gerar código eficiente, e o Python não a fornece estaticamente), a C API permite que código C manipule objetos Python diretamente de formas que quebram as premissas de um JIT, e o coletor de lixo por contagem de referências do interpretador gera um overhead de contabilidade que um JIT não consegue eliminar facilmente.

Tentativas anteriores, como o Unladen Swallow (Google, 2009) e o Pyston (Dropbox, 2014), tentaram acoplar uma compilação JIT baseada em LLVM ao CPython. As duas descobriram que o overhead de compilação do LLVM era alto demais para as cargas de trabalho típicas do Python. O LLVM foi projetado para compilação ahead-of-time de bases de código grandes; usá-lo para compilar em JIT funções Python curtas adiciona milissegundos de compilação para microssegundos de execução. A compilação custava mais do que o ganho de velocidade.

Copy-and-Patch: Um Tipo Diferente de JIT

A técnica copy-and-patch, apresentada em um artigo de pesquisa de 2021, adota uma abordagem fundamentalmente diferente para a compilação JIT. Em vez de traduzir o bytecode para uma representação intermediária e rodar passes de otimização (a abordagem do LLVM), o copy-and-patch trabalha com templates de código pré-compilados.

A ideia é esta: para cada instrução de bytecode (LOAD_FAST, BINARY_ADD, CALL_FUNCTION etc.), o compilador pré-compila uma implementação em C para código de máquina, com 'buracos' de espaço reservado para dados específicos de cada variável: alocações de registradores, valores constantes, offsets de memória. Em tempo de execução, a compilação JIT é apenas isto: copiar o template pré-compilado e preencher os buracos com os valores específicos desta função. Sem passes de otimização, sem algoritmos de alocação de registradores, sem seleção de instruções. Só memcpy e patch.

Traditional JIT pipeline:
Python bytecode
→ Parse into IR
→ Type inference
→ Optimization passes (CSE, DCE, loop unrolling, inlining...)
→ Register allocation
→ Instruction selection
→ Machine code
Total: 1-100ms per function
Copy-and-patch pipeline:
Python bytecode
→ For each instruction, copy pre-compiled template
→ Fill in holes (constants, offsets)
→ Done — machine code ready
Total: 1-100μs per function (1000x faster compilation)

O preço é a qualidade do código. O LLVM produz código de máquina altamente otimizado. O copy-and-patch produz código de máquina que é essencialmente uma versão compilada do loop do interpretador: cada instrução de bytecode continua sendo um template separado, com pouca otimização entre instruções. O código gerado é melhor que a interpretação (sem overhead de despacho, sem switch statement, com melhor predição de desvios), mas pior do que o que um compilador otimizador completo produziria.

Para o Python, esse trade-off é excelente. Funções Python costumam ser curtas, chamadas muitas vezes e levam microssegundos cada uma. Um JIT que compila em microssegundos e acelera a execução em 20 a 30% vale mais do que um que compila em milissegundos e acelera em 200%, porque o custo de compilação da primeira abordagem é amortizado quase imediatamente.

O Que Fica Mais Rápido

O JIT não acelera todo código Python de forma igual e mágica. Entender o que mais se beneficia exige entender em que o interpretador gasta seu tempo.

Overhead de despacho de bytecode. No interpretador, cada instrução de bytecode exige: buscar o próximo opcode, decodificá-lo e pular para o handler por meio de um switch statement. Esse overhead de despacho pode chegar a 30-50% do tempo total de execução em loops apertados. O JIT o elimina por completo: as instruções são compiladas em código de máquina sequencial, com saltos diretos.

Operações especializadas por tipo. O Python 3.11 introduziu o interpretador adaptativo com especialização, que substitui operações genéricas por versões específicas de tipo depois de observar os tipos realmente usados. BINARY_ADD vira BINARY_ADD_INT quando vê dois inteiros. O JIT compila essas instruções especializadas em código de máquina eficiente: uma soma de inteiros vira uma única instrução add, em vez de uma chamada de função.

Predição de desvios. O loop central de despacho do interpretador, um switch com centenas de casos, é um pesadelo para o preditor de desvios da CPU. O JIT substitui isso por um fluxo de controle direto, que a CPU prevê com precisão. Em CPUs modernas, onde um erro de predição custa 15 a 20 ciclos, só isso responde por uma parte significativa do ganho.

O que não fica mais rápido: chamadas a extensões em C (NumPy, pandas), operações de I/O (rede, disco) e operações dominadas por alocação de memória (criar milhões de objetos pequenos). Se o seu programa Python passa 95% do tempo em extensões em C e 5% em Python puro, o JIT acelera esses 5%. É mensurável, mas não transformador.

O Pipeline de Especialização

O JIT não trabalha sozinho. Ele é o estágio final de um pipeline de performance que começou com o interpretador especializado do Python 3.11 e continuou com as melhorias incrementais das versões 3.12 a 3.14.

  1. Tier 0: Interpretador. Todo código começa aqui. Interpretação padrão de bytecode com especialização adaptativa. Depois que uma função é chamada várias vezes, as instruções mais quentes são substituídas por versões especializadas por tipo.
  2. Tier 1: Bytecode compilado por JIT. O JIT copy-and-patch compila o bytecode especializado em código de máquina. Isso elimina o overhead de despacho e permite otimizações básicas, como constant folding e eliminação de código morto, dentro dos templates compilados.
  3. Tier 2 (futuro): Otimização baseada em traces. Gravar traces de execução pelos caminhos quentes do código e compilar traces inteiros, atravessando fronteiras de funções, em código de máquina otimizado. Isso está planejado, mas ainda não foi lançado.

Essa abordagem em camadas é parecida com a que o YJIT do Ruby e outros runtimes modernos de linguagens usam. Começar com interpretação rápida, passar para compilação rápida quando o código fica quente e reservar a otimização cara para os caminhos mais quentes. É a mesma intuição básica: a maior parte do código não vale a pena otimizar, então gaste o orçamento de compilação com o código que mais roda.

Impacto na Memória e no Startup

Compiladores JIT consomem memória para o código compilado. O overhead de memória do JIT copy-and-patch é modesto: o código compilado é maior que o bytecode, mas menor que o que os JITs baseados em LLVM produzem (porque não há inchaço de otimização). A implementação atual usa cerca de 1,5 a 3x a memória do bytecode que substitui, e só compila funções que são chamadas com frequência suficiente para compensar.

O tempo de startup é uma preocupação para scripts Python de vida curta. O JIT adiciona overhead para carregar a biblioteca de templates e preparar a infraestrutura de compilação. Para scripts que rodam por menos de um segundo, o overhead do JIT pode superar o ganho. O CPython resolve isso compilando funções em JIT só depois que elas foram chamadas um certo número de vezes: scripts de vida curta continuam no interpretador e não pagam o custo do JIT.

Isso é configurável. A flag -X jit controla o comportamento do JIT, e variáveis de ambiente ajustam o limiar de compilação. Para funções serverless e ferramentas de linha de comando, em que o startup importa, você pode aumentar o limiar ou desabilitar o JIT por completo. Para servidores de longa duração e scripts de processamento de dados, em que a performance em regime importa, os padrões funcionam bem.

O Que Isso Significa para o Ecossistema Python

O JIT não muda a posição do Python na hierarquia de performance: C, Rust, Go e Java continuam muito mais rápidos em trabalhos com muito processamento. O que muda é o ponto em que desenvolvedores Python precisam recorrer a essas alternativas.

Um ganho de 20 a 30% em código Python puro significa que algumas cargas de trabalho que antes exigiam extensões em C ou uma reescrita agora rodam rápido o suficiente em Python puro. Scripts de processamento de dados que levavam 10 minutos passam a levar 7. Servidores web que atendiam 1000 requisições por segundo passam a atender 1300. Não são números revolucionários, mas são a diferença entre 'Python é rápido o suficiente' e 'precisamos reescrever isso em Go'.

Mais importante ainda, a infraestrutura do JIT cria uma base para otimizações futuras. A abordagem copy-and-patch pode ser estendida com templates melhores, mais especialização e, eventualmente, compilação baseada em traces. Cada versão do Python pode trazer templates melhores sem mudar a arquitetura fundamental do JIT. O ganho de 20 a 30% no 3.15 é um piso, não um teto.

Depois de três décadas como uma das linguagens mais populares e, ao mesmo tempo, mais lentas do mundo, o CPython finalmente investe a sério em performance. O JIT não vai satisfazer o pessoal do 'Python é lento demais', porque nada vai: para algumas cargas de trabalho o Python realmente é lento, com JIT ou sem JIT. Mas para a grande maioria do código Python, em que a velocidade de execução era 'boa o suficiente, mas não ótima', o JIT a aproxima de 'realmente boa'. Isso é mais importante do que parece.