3 pontos por GN⁺ 2025-03-11 | 1 comentários | Compartilhar no WhatsApp
  • O novo interpretador com tail call do CPython pareceu inicialmente trazer um ganho médio de 10~15% no pyperformance, mas, ao ajustar a linha de base, a melhora real encolhe para cerca de 1~5%, dependendo da configuração
  • O grande salto estava mais próximo de um desvio de uma regressão no LLVM 19 do que de um efeito exclusivo da nova implementação; a variável principal foi que o Clang 19 não conseguiu reproduzir corretamente o despacho por computed goto existente
  • No Intel Raptor Lake i5-13500, o build com clang19 foi 1,09x mais lento que o com clang18, e o clang19.tc foi 1,03x mais rápido; já no Apple M1 MacBook Air, o clang19 foi 1,12x mais lento e até o clang19.tc ficou em um nível de 1,00x mais lento
  • Por causa do limite de tail duplication no LLVM, o número de saltos indiretos caiu de 332 no clang18 para 3 no clang19, fazendo com que a estrutura pretendida do interpretador baseado em computed goto praticamente desaparecesse
  • A abordagem com tail call continua sendo uma melhoria relevante, mas uma estratégia como musttail, que exige explicitamente a aplicação de uma otimização pelo compilador, pode ser mais robusta para código sensível a desempenho

O efeito da linha de base que parecia ganho de desempenho

  • O projeto CPython integrou há cerca de um mês uma nova estratégia de implementação para o interpretador de bytecode
  • Os resultados iniciais mostravam um ganho médio de 10~15% de desempenho em várias plataformas e no benchmark pyperformance
  • Análises posteriores mostraram que esse grande avanço era, em grande parte, resultado de desviar acidentalmente de uma regressão no LLVM 19
    • Em comparação com GCC, clang-18 ou LLVM 19 com certas flags de ajuste, o ganho cai para algo em torno de 1~5%
  • O interpretador com tail call realmente melhora a velocidade, mas o avanço é mais modesto do que os números iniciais sugeriam
  • Se você compilou com clang-19 ou versões posteriores, é possível que o caminho antigo estivesse de fato 10~15% mais lento
    • Simon Willison reproduziu um ganho de 10% ao comparar um build do python-build-standalone com o Python 3.13

Configuração dos benchmarks e números principais

  • Vários builds do CPython foram comparados em um servidor Intel e em um Apple M1 MacBook Air
    • O servidor Intel era um Raptor Lake i5-13500 operado pela Hetzner
    • Todos os builds usaram LTO e PGO
    • A reprodução dos builds usou uma configuração do nix
  • Os alvos de comparação foram os seguintes
    • clang18: Clang 18.1.8, computed goto
    • gcc: GCC 14.2.1, computed goto, apenas no Intel
    • clang19: Clang 19.1.7, computed goto
    • clang19.tc: Clang 19.1.7, novo interpretador com tail call
    • clang19.taildup: Clang 19.1.7, computed goto com flag de ajuste -mllvm para contornar a regressão
  • Os resultados médios do pyperformance usando clang18 como linha de base foram os seguintes
    • Raptor Lake i5-13500:
      • clang19: 1,09x mais lento
      • clang19.taildup: 1,01x mais rápido
      • clang19.tc: 1,03x mais rápido
      • gcc: 1,02x mais rápido
    • Apple M1 MacBook Air:
      • clang19: 1,12x mais lento
      • clang19.taildup: 1,02x mais lento
      • clang19.tc: 1,00x mais lento
  • O interpretador com tail call mostrou algum ganho em relação ao clang-18, mas menor do que a perda de desempenho causada pela migração para o clang-19
  • clang18.tc não pôde ser medido
    • Isso porque o interpretador com tail call depende de uma funcionalidade de compilador nova no Clang 19
    • Essa limitação exigiu mais combinações de benchmark para entender a situação

A regressão no LLVM 19 que desmontou o dispatch

  • Interpretadores tradicionais de bytecode processam opcodes com um switch dentro de um loop while
    • O compilador normalmente transforma esse switch em uma tabela de saltos com salto indireto
  • Há muito tempo se sabe que duplicar a lógica de despacho dentro do corpo de cada opcode acelera esse tipo de interpretador
    • Em vez de voltar ao início do loop no fim de cada opcode, cada um passa a ter sua própria lógica para decodificar a próxima instrução e indexar a tabela de saltos
  • Compiladores C oferecem a possibilidade de obter o endereço de labels e usá-los como computed goto, e o CPython usava esse modelo de loop do interpretador antes do trabalho com tail call
  • O Clang/LLVM, por razões de desempenho do compilador, mescla internamente vários goto de computed goto em uma única instrução indirectbr do LLVM
    • Depois, na geração de código, executa tail duplication para copiar de volta a lógica de desvio em cada ponto
    • Esse fluxo é documentado em alto nível em um post antigo do blog do LLVM
  • O LLVM 19 introduziu um limite no pass de tail duplication para evitar aumentos severos no tempo de compilação ou no uso de memória em certos casos
    • No CPython, esse limite fez o Clang deixar os saltos de despacho no estado mesclado
    • Como resultado, o objetivo da implementação baseada em computed goto foi praticamente anulado
  • Esse problema já havia sido identificado antes em implementações de outras linguagens com loops de interpretador parecidos, mas não se sabia que também afetava o CPython
  • Ao desmontar o código objeto e contar os saltos indiretos, a diferença aparece diretamente
    • _PyEval_EvalFrameDefault no build clang18: jmp * 332 vezes
    • _PyEval_EvalFrameDefault no build clang19: jmp * 3 vezes

A posição ambígua do computed goto

  • Que a mudança na lógica de tail duplication causou a regressão foi confirmado pelo fato de que, após a correção, o desempenho do nível do clang-18 foi restaurado
  • Ainda assim, a magnitude da regressão não fica totalmente explicada
    • Historicamente, já se citou que a duplicação do despacho de opcode acelera interpretadores em algo entre 20% e 100%
    • Em processadores modernos, com preditores de desvio melhores, trabalhos mais recentes mostram ganhos menores, na faixa de 2~4%
  • O Python também oferece, por opção de configuração, um interpretador antigo com um único switch
    • clang18.nocg: 1,01x mais rápido que clang18
    • clang19.nocg: 1,02x mais lento que clang18
    • clang19: 1,09x mais lento que clang18
  • O fato de clang19.nocg ter sido mais rápido que clang19 é uma reviravolta adicional
    • O Clang 18, ou o Clang 19 com as flags adequadas, também duplica a lógica de despacho no interpretador baseado em switch
  • A comparação do número de saltos indiretos também mostra essa diferença
    • clang18: 332
    • clang18.nocg: 306
    • clang19.nocg: 3
    • clang19: 3
  • No Clang moderno, o interpretador inteiro com computed goto pode ser uma complexidade desnecessária
    • Isso porque o compilador consegue fazer a mesma transformação até em código baseado em switch
    • Por outro lado, o próprio computed goto não foi suficiente para garantir essa transformação
  • O GCC 14.2.1 não duplicou o switch, mas ao usar computed goto implementou o comportamento desejado

Correção e formas de contorno

  • O pull request 114990 do LLVM foi integrado logo após a publicação do texto e corrigiu a regressão
  • Benchmarks anteriores à integração já confirmavam que essa correção restaurava o desempenho esperado
  • Nas releases anteriores à correção, é possível ajustar o limiar de interrupção do tail duplication usando a opção de tuning adicionada pelo PR que introduziu a regressão
    • No clang-19, definir esse limite como um valor muito alto pode restaurar um comportamento parecido
  • Em builds com LTO, passar essa opção é mais complicado
    • O tail duplication acontece durante a geração de código, e em builds com LTO essa geração ocorre no momento da linkedição, não na compilação
    • Por isso, a flag precisa ser passada não só ao compilador, mas também ao lld
  • Um exemplo de configuração usada foi passar -mllvm -tail-dup-pred-size=5000 em OPT e LDFLAGS na etapa de ./configure

O problema da linha de base revelado pelo benchmarking

  • Benchmarks conseguem medir com precisão a diferença de desempenho entre builds específicos, mas expandir esse resultado para “ganho geral de desempenho” exige hipóteses adicionais
  • Os benchmarks do interpretador com tail call mostraram resultados 10~15% mais rápidos que o interpretador anterior com computed goto, mas a linha de base era complexa demais para generalizar essa conclusão
  • Em trabalho de desempenho, com o que você está comparando é repetidamente uma questão difícil
    • Mesmo com entendimento teórico da melhor abordagem conhecida, ajustar corretamente sistema operacional, opções do compilador e flags é outro problema
    • Benchmarks públicos feitos em hardware antigo ou em escalas difíceis de reproduzir podem não ser adequados para comparação direta
  • Em artigos de machine learning, ao se afirmar uma melhora algorítmica, muitas vezes a primeira pergunta importante não é “o que foi feito”, mas “com qual linha de base foi comparado”
  • Resultados impressionantes podem surgir facilmente quando a comparação é feita com uma linha de base mal ajustada

Compiladores otimizadores e musttail

  • O caso do computed goto mostra que as expectativas sobre compiladores otimizadores podem entrar em conflito
    • O compilador deve respeitar a intenção do programador e preservar o mesmo comportamento
    • Ao mesmo tempo, também precisa fazer transformações complexas e pouco intuitivas para tornar o código mais rápido
  • O clang-19 compilou corretamente o interpretador com computed goto do ponto de vista do comportamento do programa, mas produziu uma saída totalmente diferente da intenção de otimização
  • Outras versões do Clang aplicam a mesma otimização pretendida até mesmo a um interpretador simples baseado em switch()
  • No nível do código-fonte, computed goto e, no nível do código de máquina, duplicação do despacho parecem conceitos quase ortogonais
    • Como o resultado da execução é o mesmo, as ferramentas atuais têm dificuldade para representar essa diferença de forma consistente
  • O interpretador com tail call se baseia no atributo musttail
    • musttail não altera o comportamento observável tradicional do programa, mas é mais próximo de uma conversa com o otimizador
    • O compilador precisa ser capaz de aplicar uma otimização específica e, se isso não acontecer, a compilação deve falhar
  • Esse tipo de abordagem pode se tornar um estilo mais robusto para escrever código sensível a desempenho mesmo com a evolução dos compiladores
  • Também vale considerar se um atributo hipotético como [[clang::musttailduplicate]] poderia substituir o computed goto no loop while do interpretador

A reprodutibilidade e os limites oferecidos pelo nix

  • O nix foi de grande ajuda para gerenciar vários builds do interpretador Python
    • Durante o experimento, foram compiladas e medidas dezenas de versões do interpretador Python com quatro compiladores (gcc, clang-18, clang-19, clang-20) e várias combinações de flags
  • Com nix, foi possível manter essas versões em paralelo de forma reproduzível e isolada
    • Era possível saber com confiança de qual compilador e de quais flags cada build vinha
    • A definição da matriz de builds também pôde ser gerenciada com abstrações curtas
  • Também foi possível compilar um LLVM customizado com o patch de correção do bug e usá-lo para gerar builds do Python com cerca de 10 linhas de código
  • Ainda assim, há desvantagens
    • O nix difere do modo comum de uso de software, então é difícil descartar totalmente a possibilidade de que essas diferenças tenham influenciado os benchmarks ou as conclusões
    • Por exemplo, o nix compila projetos com certas flags de hardening por padrão, e isso levou à descoberta inicial de que essas flags afetavam desproporcionalmente o interpretador com tail call
  • O Nix é forte em escalabilidade e customização, mas encontrar a forma de aplicar certas customizações exigiu muita tentativa e erro e exploração do código-fonte do nixpkgs

1 comentários

 
GN⁺ 2025-03-11
Opiniões no Hacker News
  • Sou o autor do PR que colocou o interpretador de chamadas de cauda no CPython
    Antes de tudo, agradeço ao Nelson, que passou quase um mês descobrindo a causa desse problema
    Nem eu, e provavelmente nem a equipe do CPython, esperávamos que houvesse um bug desses no compilador usado como linha de base; cometi um grande erro e estou muito envergonhado e arrependido
    Também publiquei um pedido de desculpas: https://fidget-spinner.github.io/posts/apology-tail-call.htm...

    • Ao ler “cometi um grande erro e estou muito envergonhado e arrependido”, achei que você tivesse prejudicado o desempenho do CPython, mas na prática não foi nada disso
      Foi anunciado como melhoria de desempenho de 10–15%, mas em compiladores sem o bug parece estar mais perto de 1–5%; e esses números nem estavam completamente errados, apenas eram verdadeiros sob certas condições
      Você criou a melhoria, mediu e teve o PR revisado, então fez o que precisava fazer. Acontece que a versão do clang usada na medição causou uma interpretação equivocada dos números, mas isso parece um erro razoável em que qualquer um poderia cair
      Ainda assim, você trouxe uma melhoria de desempenho significativa e também encontrou uma regressão no compilador; comparado a isso, os números incorretos parecem algo menor. Também não está claro quem foi realmente prejudicado por isso, então não me parece algo que exigisse um pedido de desculpas
    • Para constar, depois que aquele post do blog foi escrito, a correção foi mesclada ;)
      Se 3–5% se mantiverem em um sistema antigo como o interpretador Python, isso por si só já é um grande feito, então dá para se orgulhar bastante
      Depois de uns 30 anos, passei a desconfiar, de início, de melhorias de desempenho significativas em sistemas que existem há muito tempo, especialmente quando passam de 1%
      Melhorias reais existem, mas não são comuns; muitas vezes, apenas deslocaram o tempo para outro lugar e isso não aparece no benchmark. Além disso, benchmarks são feitos em ambientes controlados para isolar o efeito, mas software real roda em VMs ou desktops junto com todo tipo de outras coisas
      Vi muitas vezes uma melhoria que parecia claramente grande em um ambiente isolado desaparecer, ou ficar negativa, ao entrar em produção
      No CPython isso é ainda mais difícil porque ele precisa mirar vários ambientes, e não há um único alvo de produção para dizer “se não ficou mais rápido em produção, então não ficou realmente mais rápido”. Tentar melhorar desempenho nesse mundo é realmente difícil
      No fim, ajuste e medição de desempenho são muito difíceis, e a única coisa de que talvez valha se desculpar é ter aprendido esse fato
      Espero que você não tenha medo de errar. Todo mundo erra mesmo. Basta dizer, como agora, “parece que estragamos isso”, e descobrir como lidar com o problema e como evitá-lo no futuro
      [1] Isso é comum não só em desempenho, mas também em processos humanos. Por exemplo, uma equipe de ferramenta de revisão de código pode dizer “reduzimos o tempo de revisão de código em 15% e aceleramos o fluxo de trabalho de todos”, quando na verdade criou mais trabalho em outra parte do sistema, o fluxo geral não ficou mais rápido, e os 15% foram apenas movidos para um lugar que não foi medido
    • Vejo que uma das principais motivações do desenho do interpretador de chamadas de cauda é ficar menos vulnerável aos caprichos do otimizador. O artigo original que tratava dessa técnica (https://blog.reverberate.org/2021/04/21/musttail-efficient-i...) também explicava isso dessa forma
      Em tese, com esse grafo de fluxo de controle e esse perfil, o compilador deveria ter informação suficiente para gerar código ótimo para um interpretador tradicional baseado em switch(). Mas, na prática, quando uma função fica tão grande e interligada assim, você acaba brigando com o compilador
      Ele faz spill de variáveis importantes que você queria manter em registradores, puxa para cima manipulações de stack frame que você queria encolher em torno de chamadas de fallback, e mescla caminhos de código idênticos que você queria separar por causa da previsão de desvios. Pode parecer tocar piano usando luvas
      Aqui também aconteceu exatamente essa “mesclagem de caminhos de código idênticos”, e o compilador “com bug” mesclou os caminhos iguais, piorando o desempenho
      O compilador “corrigido” não faz mais isso, mas essa correção, no fim, está mais para um ajuste de heurísticas internas do compilador. Não há garantia de que este compilador, ou outros, continuem mantendo heurísticas favoráveis a nós no futuro
      Já o interpretador de chamadas de cauda permite expressar no próprio interpretador o padrão de código de máquina desejado. Usando os atributos musttail, noinline e preserve_none juntos, dá para restringir o problema de modo a ficar muito menos sujeito às heurísticas do otimizador
      Por isso, o benefício do interpretador de chamadas de cauda vai além de uma simples melhoria de desempenho de 3–5%; em alguns compiladores, pode ser uma melhoria de desempenho confiável ainda maior
    • Tenho respeito por quem consegue dizer “desculpe, eu errei”. Detesto essa cultura de fingir até dar certo e agir como se tivesse sido um sucesso, que hoje parece ser o padrão
    • Fico curioso para saber por que a regressão de desempenho da linha de base não apareceu na página de benchmarks do faster-cpython [0], ou se apareceu
      Será que os benchmarks poderiam ser melhorados para evitar algo parecido?
      [0] https://github.com/faster-cpython/benchmarking-public
  • Fazer benchmarking direito é absurdamente difícil. Há fatores demais que enganam as pessoas.
    Recentemente, achei que tinha encontrado uma forma de tornar certo algoritmo cerca de 15% mais rápido. Pelo menos era isso que todos os benchmarks diziam.
    Mas, quando dupliquei a função mais rápida dentro do harness de teste e, na prática, nem a chamei — chamando apenas a versão original mais lenta —, ela ainda assim ficou 15% mais rápida. Ou seja, código que nem sequer era executado tornou o código original mais rápido.
    Naturalmente, era um problema de layout de código e memória: alguma coisa se moveu e passou a se encaixar melhor no cache da CPU.
    É realmente difícil saber se o ganho de velocidade veio porque o código ficou de fato “melhor” ou porque, em algum lugar, você teve a sorte de conseguir um alinhamento melhor.
    Casey Muratori está escrevendo uma série muito interessante sobre esse tema no Substack.

    • É surpreendente que essa loteria do linker tenha gerado até 15% de melhoria. Fico curioso para saber em que casos aparecem ganhos tão grandes, se isso é raro e como vocês acabaram determinando a causa.
    • Lembro vagamente de um projeto de benchmarking que randomizava deliberadamente algumas decisões do compilador, para estimar de forma mais estável quão bem o código realmente performa e ficar menos sujeito a ter ganhado ou perdido na loteria do linker.
    • Aleksey Shipilёv, que atuou por muito tempo como “engenheiro de performance” em Java, escreveu e palestrou bastante sobre as dificuldades de benchmarking. Recomendo fortemente os posts e apresentações dele.
  • Parabéns ao autor por investigar a fundo e revelar o que estava acontecendo de fato. O interpretador com chamadas de cauda do Python 3.14 ainda é uma boa melhoria, e alguns pontos percentuais de ganho em um runtime de linguagem são conquistas difíceis.
    Só não era aquele almoço grátis mágico de 15%.
    Mais importante: este caso mostra muito bem a importância do rigor em benchmarking e de testar em vários ambientes. Também revelou um bug de compilador que pode beneficiar todo mundo.
    É o tipo de análise aprofundada que faz a gente revalidar a próxima grande alegação de ganho de desempenho. Uma pergunta para refletir é: entre os muitos resultados de “X% mais rápido” por aí, quantos na verdade são artefatos de benchmark ou regressões desconhecidas?
    Como podemos evitar melhor essas armadilhas daqui para frente?

    • A dúvida maior é por que a queda de 10% no desempenho do Python não foi detectada quando esse recurso defeituoso do compilador entrou.
      O compilador em si não é benchmarkado? Os benchmarks existentes do lado do compilador ou do Python não usavam esse compilador?
  • Este é um bom exemplo de como é inadequado dizer que C é “próximo da máquina” ou “assembly portável”. Otimizadores modernos mudam a lógica agressivamente quando não há efeito observável.
    O texto também dizia que “o clang-19 compila o interpretador com computed goto de forma ‘correta’, no sentido de que o binário resultante produz todos os valores esperados, mas, ao mesmo tempo, essa saída contraria completamente a intenção da otimização. Além disso, outras versões do compilador aplicam otimizações ao interpretador ‘ingênuo’ baseado em switch(), implementando exatamente a mesma otimização que nós ‘pretendíamos’ ao reescrever o código-fonte”.

    • Do ponto de vista de outras linguagens de programação de sistemas dos anos 80 e 90, C ainda é bastante próximo de assembly portável.
      Em C, dá para confiar que a += 1 incrementa um valor numérico, mas a mesma expressão em C++ pode alocar memória, desenrolar a pilha de chamadas ou fazer coisas desconhecidas. Da mesma forma, a = "a" em C é uma simples atribuição de ponteiro, enquanto em C++ pode envolver alocação de memória etc.
      A expressão “C é assembly portável” não significa que cada instrução seja compilada diretamente para uma instrução de máquina equivalente.
    • “Sem efeito observável” virou um post de blog de 10 mil palavras.
  • Não surpreende que, ao mexer na estrutura de laços, o compilador tenha feito com que o interpretador com chamadas de cauda inteiro não fosse tão eficaz quanto anunciado.

    1. A arquitetura da CPU e a versão importam muito. 95% do problema é organizar o código de despacho de instruções para que o preditor de desvios funcione de forma ótima, e C não foi originalmente criada para dar suporte a esse tipo de coisa.
    2. A máquina abstrata de C também não é de baixo nível o suficiente para expressar bem a intenção. Qualquer implementação fica sensível demais às características de um compilador específico e de uma versão específica.
      Implementações paranoicas de interpretadores às vezes voltam a escrever assembly diretamente. O LuaJIT é famoso por ter implementado um sistema de macros para tornar uma implementação de loop em assembly muito eficiente portável entre arquiteturas. É por isso que mexer com esse tipo de coisa também é divertido.
      Alguns anos atrás, também escrevi um texto e testes sobre formas populares de implementar loops de interpretador:
      https://github.com/vkazanov/bytecode-interpreters-post
    • Como autor, ao escrever este texto aprendi que a afirmação de que “95% do problema é organizar o código de despacho de instruções para que o preditor de desvios funcione de forma ótima” já não é mais verdadeira.
      Preditores de desvios modernos conseguem prever quase perfeitamente até mesmo um único salto indireto, desde que o trecho de execução seja longo o suficiente e o comportamento do próprio código interpretado seja estável.
      Há um artigo que estudou isso tanto em hardware real quanto em um preditor de desvios simulado específico: https://inria.hal.science/hal-01100647/document
      Os experimentos que fiz neste projeto também sustentam anedoticamente a mesma conclusão. Não consegui incluir no texto, mas examinei alguns interpretadores com contadores de hardware da CPU e perf stat, e erros de predição de desvios não apareceram como fator dominante.
  • Avaliar a performance de builds do Python é extremamente difícil. Há técnicas de build demais que podem melhorar o desempenho.
    Recentemente, o pessoal da astral também esbarrou nesse problema ao mostrar que os builds do conda-forge eram visivelmente mais rápidos do que a maioria:
    https://github.com/astral-sh/python-build-standalone/pull/54...
    Fico curioso para saber como o interpretador com chamadas de cauda se comporta quando usado junto com outras otimizações de build já existentes.

  • Discussões relacionadas:
    https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-... --> https://news.ycombinator.com/item?id=42999672 (66 pontos | 25 dias atrás | 22 comentários)
    https://blog.reverberate.org/2025/02/10/tail-call-updates.ht... --> https://news.ycombinator.com/item?id=43076088 (124 pontos | 18 dias atrás | 92 comentários)

  • Bom artigo. Um detalhe me chamou atenção
    Em um dos artigos citados, https://simonwillison.net/2025/Feb/13/python-3140a5/, ele escreveu que “o 3.14.0a5 foi 1,12x mais rápido que o 3.13 no benchmark, no meu M2 MacBook Pro extremamente sobrecarregado”
    Essa parte é bem confusa. Quer dizer que ele rodou o benchmark enquanto o computador estava sobrecarregado com outros processos? Então o resultado não fica completamente pouco confiável?
    Eu achava que benchmarks desse tipo eram executados em ambientes muito controlados para eliminar variáveis externas

  • Algumas pessoas aqui chamam 10% de “grande” e 1% de “normal”, mas uma otimização como inlining parcial do Fibonacci duplamente recursivo pode reduzir exponencialmente a carga de trabalho e o tempo reais
    Com argumentos de dois dígitos, pode passar de 10x, ou seja, milhares por cento. Estritamente falando, é exponencial em relação à diferença de profundidade da recursão, não ao tamanho do problema [1]
    Compiladores C também podem reagir de forma muito sensível a métricas de inlining de código, então se esse enorme ganho de velocidade aparece de fato pode depender muito da forma do código
    Portanto, parte do problema é que as CPUs ficaram muito sofisticadas e complexas, mas outro lado é que os compiladores além de -O0 ou -O1 também ficaram sofisticados e complexos
    O artigo é bom e vale a leitura, mas também é um entre muitos exemplos de como a interação entre duas coisas complexas pode produzir resultados muito surpreendentes. Isso também vale fora da computação
    As pessoas têm uma forte tendência a simplificar demais essa lição, não importa quantas vezes ela se repita
    Além disso, o artigo usa pelo menos duas CPUs, Intel e Apple M1, e dois compiladores, gcc e clang, mas ambientes reais de distribuição podem ter muito mais gerações e implementações de Intel, AMD e ARM, além de outros compiladores. Ou seja, ele amostra só uma parte minúscula da complexidade total
    Para ser mais científico, especialmente em diferenças como “1,01x”, a medição de tempo deveria ter algum tipo de barra de erro. O desvio padrão da média, ou, nesses casos, talvez o desvio padrão do valor mínimo seja melhor [2]
    Para reduzir o erro de medição, provavelmente também seria necessário fixar o agendamento em núcleos de CPU pelo SO
    [1] https://stackoverflow.com/questions/360748/computational-com...
    [2] https://github.com/c-blake/bu/blob/main/doc/tim.md

  • Recentemente fiz benchmarks do Python 3.9 ao 3.13, e até o 3.11 ele continuou melhorando
    Mas Python 3.12 e 3.13 ficaram cerca de 10% mais lentos que o 3.11
    Achei que meu benchmark próprio talvez não fosse bom o suficiente, mas mesmo assim implantei em um serviço crítico, e as métricas coletadas mostraram a mesma mudança
    Alguém passou pelo mesmo problema?