3 pontos por GN⁺ 2025-03-11 | 1 comentários | Compartilhar no WhatsApp
  • O projeto CPython introduziu recentemente uma nova estratégia de implementação para o interpretador de bytecode. Os resultados iniciais mostraram um ganho médio de desempenho de 10–15% em várias plataformas
  • No entanto, esse ganho de desempenho foi em grande parte resultado de contornar um problema de regressão no LLVM 19. Quando comparado com referências melhores (por exemplo, GCC, clang-18, LLVM 19 com flags de ajuste específicas), o ganho cai para 1–5%

Resultados de desempenho

  • Foram feitos benchmarks de várias builds do interpretador CPython usando diferentes compiladores e opções de configuração. Os testes foram realizados em um servidor Intel e em um Apple M1 Macbook Air.
  • Todas as builds usaram LTO e PGO. Foi usada a média reportada por pypeformance/pyperf compare_to, tomando clang18 como referência.
  • Comparação de desempenho dos compiladores
    • Apple M1 Macbook Air :
      • clang18: referência
      • clang19: 1,12x mais lento
      • clang19.taildup: 1,02x mais lento
      • clang19.tc: 1,00x mais lento
      • gcc: N/A
  • O interpretador com tail-call ainda mostrou ganho de velocidade em relação ao clang-18, mas a queda de desempenho ao migrar para o clang-19 foi ainda mais dramática.

Regressão no LLVM

Contexto rápido

  • Interpretadores tradicionais de bytecode consistem em uma instrução switch dentro de um loop while. A maioria dos compiladores compila switch como uma tabela de saltos.
  • Compiladores C modernos oferecem suporte ao padrão de obter o endereço de labels e usá-lo como um "computed goto". O CPython usava esse padrão até o trabalho com tail-call.

Regressão no LLVM 19

  • O LLVM 19 impôs limites ao passe de tail-duplication, fazendo com que a duplicação fosse interrompida quando o tamanho do IR ultrapassasse um certo limite. Com isso, no CPython todos os saltos de dispatch foram mesclados, anulando completamente o propósito da implementação baseada em goto calculado.

Outras anomalias

  • Há confiança de que a mudança na lógica de duplicação de tail-call causou a regressão, mas isso não explica completamente a magnitude da regressão.
  • Em processadores modernos, ganhos de velocidade de 2–4% são mais comuns.

O computed goto é necessário?

  • O benchmark clang19.nocg afirma ser mais rápido que clang19. Isso mostra que o compilador pode aplicar as mesmas otimizações usando um interpretador baseado em switch.

Correção

  • O pull request 114990 do LLVM corrigiu a regressão. Essa correção restaabelece o desempenho esperado.

Reflexões

Sobre benchmarking

  • Ao otimizar sistemas, são definidos benchmarks e metodologias de benchmarking, e as mudanças propostas são avaliadas com base neles.
  • Benchmarks exigem mais suposições e confiança para generalizar um ponto de dados específico.

Linha de base

  • Ao propor uma nova solução ou método, é comum comparar com a "melhor abordagem atualmente conhecida".

Sobre engenharia de software

  • Sistemas de software são complexos, interconectados e mudam rapidamente.
  • Compiladores otimizadores vivem uma tensão entre respeitar a intenção do programador e ainda assim otimizar o código.

Compiladores otimizadores

  • O atributo musttail representa um novo tipo de recurso de compilador relacionado à otimização. Isso pode oferecer um estilo mais poderoso para escrever código sensível a desempenho.

Mais uma observação sobre nix

  • O nix foi muito útil neste projeto. Ele ajudou bastante a gerenciar e compilar várias versões do interpretador Python.

1 comentários

 
GN⁺ 2025-03-11
Comentários do Hacker News
  • Olá. Sou o autor do PR que introduziu o interpretador com tail-calling no CPython

    • Primeiro, gostaria de agradecer ao Nelson, que passou quase um mês procurando a causa raiz desse problema
    • Também me sinto muito envergonhado e peço desculpas por ter cometido um erro tão grande
    • A equipe do CPython também não esperava que o compilador que usamos tivesse um bug desse tipo
    • Publiquei aqui um post de desculpas no blog: link
  • Fazer benchmarking direito é realmente muito difícil

    • Recentemente, descobri uma forma de tornar um algoritmo cerca de 15% mais rápido
    • Mas, durante os testes, o código original ficou 15% mais rápido mesmo sem chamar a versão mais rápida da função
    • Isso aconteceu por causa do layout do código e da memória, que acabou se alinhando melhor com o cache da CPU
    • Casey Muratori está fazendo uma série interessante sobre esse tema
  • Parabéns ao autor por ter investigado a fundo a verdade por trás desse problema

    • O interpretador com tail-call do Python 3.14 ainda é uma boa melhoria
    • Esse caso mostrou a importância de benchmarking rigoroso e de testar em ambientes variados
    • Além disso, agora foi encontrado um bug de compilador que pode beneficiar todo mundo
    • Fico me perguntando quantos resultados de "X% mais rápido" na verdade vêm de artefatos de benchmark ou regressões ainda desconhecidas
  • Este é um bom exemplo de que C não é uma linguagem "próxima da máquina"

    • O clang-19 compila corretamente um interpretador com computed goto, mas gera uma saída completamente diferente da intenção da otimização
    • Outras versões de compiladores também aplicam otimizações a um interpretador baseado em switch() "ingênuo"
  • Ao ajustar a forma como o compilador organiza o loop, o interpretador com tail-call não é tão eficaz quanto foi anunciado

    • A arquitetura e a versão da CPU importam muito
    • A máquina abstrata de C não é de baixo nível o suficiente para expressar corretamente a intenção
    • Algumas implementações de interpretadores particularmente paranoicas voltam a escrever assembly diretamente
    • O luajit implementa um sistema de macros para tornar implementações eficientes de loops em assembly portáveis entre arquiteturas
  • Avaliar o desempenho de builds do Python é muito difícil

    • Recentemente, a equipe da Astral mostrou que os builds do conda-forge são mais rápidos que a maioria dos outros builds
    • Fico curioso para saber como o interpretador com tail-call funciona em conjunto com outras otimizações de build
  • Discussões relacionadas:

  • Excelente artigo

    • Em um dos artigos citados, é mencionado que o 3.14.0a5 é 1,12x mais rápido que o 3.13
    • Fiquei confuso se o benchmark foi executado enquanto a máquina estava sobrecarregada por outros processos
    • Benchmarks deveriam ser executados em ambientes rigorosamente controlados para eliminar variáveis externas
  • Recentemente, fiz benchmarking do Python 3.9 ao 3.13

    • Até o 3.11, o desempenho melhorou, mas o 3.12 e o 3.13 eram cerca de 10% mais lentos que o 3.11
    • Achei que meu benchmark talvez não fosse suficiente, mas observei a mesma mudança ao implantar isso em um serviço principal
  • Fico me perguntando como esse tipo de otimização se relaciona com tail-call optimization

    • A implementação da jump table do interpretador não deveria afetar a criação de stack frames