2 pontos por GN⁺ 2024-05-13 | 1 comentários | Compartilhar no WhatsApp
  • Implementações alternativas, como runtimes de linguagem e JITs, mesmo com bom desempenho, podem ter adoção limitada porque precisam acompanhar continuamente as mudanças da implementação canônica e as expectativas dos usuários
  • PyPy, LuaJIT e TruffleRuby mostraram alta performance de execução, mas a lacuna de compatibilidade e o custo de acompanhar novos recursos se tornaram barreiras para implantação real
  • O YJIT escolheu entrar dentro do CRuby, em vez de ser uma implementação Ruby separada, e foi feito para ser 100% compatível com os recursos do CRuby desde o início; hoje está implantado em Shopify, Discourse, GitHub e outros
  • Escolhas como Crystal, que é muito parecido com uma linguagem existente mas não compatível com ela, podem fazer o usuário esbarrar repetidamente na diferença entre “quase Ruby, mas não Ruby”
  • Em áreas como parsers JSON ou JavaScript, onde há padrões públicos separados das implementações, o peso de uma implementação alternativa diminui; já em ecossistemas onde a implementação canônica é, na prática, o padrão, é preciso outra estratégia

A armadilha recorrente das implementações alternativas

  • No mundo do software, repete-se o caso de projetos que começaram como uma implementação alternativa melhor de um sistema existente, mas acabaram presos à sombra da implementação canônica
  • Uma implementação alternativa é comparada à implementação canônica (canonical implementation), aceita como padrão em funcionalidade, desempenho, ecossistema e expectativas dos usuários
  • Quando a implementação canônica continua mudando, a implementação alternativa precisa gastar muita energia acompanhando essas mudanças em vez de definir sua própria direção
  • Quando se adiciona uma implementação JIT a uma linguagem tradicionalmente interpretada, novos recursos tendem a entrar mais rápido no interpretador, aumentando o peso de acompanhamento do lado do JIT

O padrão visto em PyPy, LuaJIT e TruffleRuby

  • O PyPy é um compilador JIT avançado para Python que podia oferecer grandes ganhos de velocidade em relação ao CPython, mas o uso real foi muito pequeno
    • Python é um alvo móvel, com novas versões e recursos do CPython saindo regularmente
    • O PyPy teve dificuldade para acompanhar isso e acabou ficando sempre várias versões de Python atrás
    • Para tornar um software Python compatível com o PyPy, era preciso limitar os recursos de Python que podiam ser usados, e a maioria dos programadores Python não queria se preocupar com isso
  • O LuaJIT ofereceu um grande ganho de desempenho em relação à implementação Lua baseada em interpretador, conquistando alta reputação e algum nível de adoção real
    • O criador do LuaJIT, Mike Pall, é visto por muita gente como um programador excepcional
    • Conforme a linguagem Lua continuou adicionando novos recursos, o LuaJIT também ficou várias versões atrás
    • Por isso, alguns usuários de Lua evitam usar LuaJIT
    • Lua é conhecida pelo minimalismo, mas não houve esforço para desacelerar a adição de recursos novos nem para coordenar isso com Mike Pall
  • O TruffleRuby exibiu alguns dos números de desempenho mais impressionantes entre os JITs de Ruby, mas teve implantação limitada
    • Um dos motivos práticos é que o tempo de aquecimento do TruffleRuby é muito maior que o do CRuby
    • Como o CRuby continuou adicionando recursos, os contribuidores do TruffleRuby precisaram correr atrás para acompanhar
    • Usuários de Ruby veem o CRuby como a implementação canônica, e tendem a considerar pouco atraente uma implementação que não seja totalmente compatível

O caminho diferente escolhido pelo YJIT

  • O YJIT começou como mais um JIT para Ruby, mas foi construído dentro do próprio CRuby, e não como uma implementação separada
  • Essa escolha trouxe vários trade-offs de projeto, mas permitiu que o YJIT fosse 100% compatível com todos os recursos do CRuby desde o começo
  • Hoje, o YJIT é o JIT “oficial” do Ruby e está implantado em Shopify, Discourse, GitHub e outros
  • Quem visita github.com ou uma loja da Shopify já interagiu com o YJIT
  • Até agora, o YJIT foi o compilador JIT de Ruby com maior sucesso, e a compatibilidade teve papel central nesse resultado

“Se não pode vencê-los, junte-se a eles” não basta

  • Quando você se posiciona como uma implementação alternativa, há grande chance de entrar num jogo permanente de correr atrás, sempre na sombra da implementação canônica
  • Se o projeto canônico continua evoluindo, a implementação alternativa acaba tendo poder limitado de decisão sobre a direção do próprio projeto e precisa apenas segui-lo
  • Juntar-se à implementação canônica pode gerar resultados melhores, mas isso sozinho não resolve todos os casos
  • O Crystal, no ecossistema Ruby, é uma linguagem compilada estaticamente com sintaxe parecida com Ruby e uso de inferência de tipos
    • O Crystal se separou do Ruby deliberadamente sem buscar compatibilidade com Ruby
    • Para Rubyistas, ele parece uma linguagem “quase Ruby, mas não exatamente Ruby”, e na prática há muitas diferenças sutis e incompatibilidades
    • Essa semelhança abala as expectativas dos usuários e gera confusão
    • O Crystal talvez pudesse ter tido resultado melhor se não tivesse sido divulgado desde o início como algo parecido com Ruby

Evitar a competição e seguir a própria direção

  • A frase de Peter Thiel, “competition is for losers”, é usada no contexto de não se colocar desnecessariamente numa posição em que se precise competir
  • Daí vem o conselho de que, ao criar uma nova linguagem de programação, é melhor não tentar fazer um subconjunto de Python ou algo superficialmente muito próximo de uma linguagem existente
  • Ao criar algo próprio, você pode evoluir o sistema no seu ritmo e na sua direção, sem ficar preso à expectativa de igualar desempenho, conjunto de recursos e ecossistema de bibliotecas de outra implementação
  • Esse conselho se aplica quando existe uma implementação canônica para a linguagem ou sistema
  • Áreas com padrões públicos podem ser exceção
    • Parsers JSON são um alvo viável para implementações próprias porque têm uma especificação clara, relativamente pequena e que não muda rápido
    • JavaScript pode ter várias implementações baseadas em navegadores porque existe um órgão de padronização externo que gerencia a especificação da linguagem
    • Os responsáveis pelo padrão de JS entendem que implementações com compilação JIT são importantes para desempenho e conduzem a evolução da linguagem levando isso em conta
    • Eles não estão jogando o jogo de adicionar o máximo possível de recursos novos o mais rápido possível

1 comentários

 
GN⁺ 2024-05-13
Opiniões no Hacker News
  • Há mais um ponto importante que o OP deixou passar. Ao criar uma implementação alternativa, normalmente a arquitetura difere da implementação de referência, e algo que é fácil na implementação de referência pode ser muito difícil na sua
    Por exemplo, suponha que um software proprietário para relatórios financeiros salve documentos em um formato binário estranho. Ao criar uma alternativa gratuita, você escolhe uma arquitetura que lê o documento inteiro para a memória e regrava o arquivo inteiro ao salvar; já o original pode ter sido criado em uma época de pouca RAM, lendo e escrevendo apenas a seção em que o usuário está trabalhando e até permitindo modificações in-place
    Depois, se o original adicionar um recurso para inserir anexos no documento, arquivos grandes como gravações de calls com investidores ou PDFs escaneados de centenas de páginas funcionam bem graças ao carregamento por seções. Já a sua implementação desserializa o documento inteiro, então o problema aparece assim que o documento fica maior que a RAM do usuário, e uma mudança que no original um desenvolvedor faria em uma semana pode exigir redesenhar o software inteiro

    • Trabalhei em um setor em que todo componente essencial de software precisa ter duas implementações independentes e participei de várias reimplementações; já vi recursos em que a implementação original claramente fazia X e isso não se encaixava bem na nossa arquitetura. Mesmo assim, nunca tivemos de redesenhar o programa por causa desse recurso; era mais um caso de uma funcionalidade trivial em um lado dar um pouco mais de trabalho no outro
      Por outro lado, também houve recursos que eram mais difíceis de implementar no original, mas se tornaram triviais na segunda implementação. Só houve um grande redesenho, e foi justamente na implementação original, que tinha uma premissa que levava a uma explosão exponencial
      Um caso público é o problema de executar aplicações Windows no Linux. O kernel Linux é uma implementação completamente diferente do NT e nem tem como objetivo a compatibilidade, mas executar apps Windows não exige redesenhar o kernel inteiro. Bastam alguns recursos de kernel relativamente genéricos e uma camada de compatibilidade em espaço de usuário. O Wine dá muito trabalho para escrever e manter, mas muito menos do que a própria implementação do Windows, e roda sobre uma plataforma que não foi projetada para ser compatível com Windows. Ainda assim, como o texto diz, é preciso ficar correndo atrás do Windows, e também aparece código que depende de bugs da implementação de referência; então, para ter compatibilidade até com os bugs, primeiro é preciso descobrir quais bugs devem ser implementados
    • O autor parece já ter abordado esse ponto na parte sobre a diferença de dificuldade para implementar novos recursos em linguagens interpretadas e compiladas
    • Sim. Esse é o problema clássico de tornar Python rápido. O CPython começou como um interpretador simples que, sem otimizações, apenas seguia o que o código mandava; tudo é um dicionário e praticamente não há concorrência real
      Por isso, qualquer código pode consertar outra coisa em tempo de execução. Na prática isso não é usado tanto assim, mas, se tentarem remover, as pessoas fazem um escândalo. Uma implementação que compile Python de verdade precisa lidar até com a situação em que uma thread de repente muda algo por baixo de outra thread
    • Do ponto de vista do dilema do inovador, a implementação de referência também pode ser a de um concorrente. Agora que você conhece melhor o mercado, se escolher uma arquitetura melhor, pode se colocar em posição de adicionar novos recursos de forma mais barata e rápida que o concorrente
      É uma das formas pelas quais uma empresa pequena aumenta o peso carregado por uma grande, e uma empresa que deixou menos dívida técnica se acumular também pode fazer isso. Também é um dos raros momentos em que dá para mostrar a dívida técnica claramente à diretoria, porque você pode dizer: “vamos levar mais tempo que a Acme para implementar esse recurso”
    • Às vezes essas diferenças de arquitetura são intencionais. Por exemplo, as versões GNU de muitos utilitários Unix básicos adotaram trade-offs completamente diferentes das versões originais do Unix e do BSD para evitar suspeitas de violação de copyright
  • Concordo com a tese de “não tente criar um subconjunto de Python”. Projetos divulgados como “é Python, mas X é melhor” sempre têm dificuldade para competir com a implementação de referência, especialmente se X for velocidade. Quem usa uma linguagem de tipagem dinâmica muitas vezes acaba não se importando tanto assim com velocidade de execução
    Mas implementações alternativas nem sempre fracassam. O MicroPython parece ter tido bastante sucesso, embora quase não ofereça suporte além do nível do Python 3.4. Isso porque ele foi projetado para rodar em microcontroladores e não compete com o CPython, mas com outros ambientes de programação para microcontroladores
    Ainda assim, imagino que os mantenedores do MicroPython recebam muitos pedidos por recursos mais recentes do Python. Em certo momento pensei em uma implementação alternativa de Python, leve e focada em embedding, para ser embutida em aplicações; nesse caso, a ideia também era competir com Lua, não com o CPython. Mas o pedido de recurso número 1 era: “tem suporte a NumPy?”

    • Curiosamente, o MicroPython agora também vem recebendo interesse e pedidos para uso na web, graças ao excelente trabalho do PyScript. Mas, ao contrário de microcontroladores com 1 MB de RAM total, na web se espera que código existente rode e que haja uma experiência e compatibilidade completas com o CPython, o que torna tudo muito mais difícil
      Ao mesmo tempo, parece haver trabalho em andamento do lado do CPython para funcionar melhor no front-end, e o principal ponto de dor é o tamanho dos pacotes
  • Aprendi algo parecido ao criar uma startup. Se fosse fazer de novo, teria evitado ativamente os recursos básicos de entrada do nosso setor
    Em vez disso, deveríamos ter criado apenas o mínimo para dar confiança de que nossa arquitetura conseguiria atender requisitos de estilo enterprise, e concentrado tudo nos diferenciais capazes de provocar uma reação do tipo “ah, dá para ver até onde isso pode ir”. Não em recursos que recebem a resposta “é só uma cópia de X”

    • Entendo a ideia, mas você está usando o termo “table stakes” de forma errada. Por definição, isso é aquilo que é obrigatório colocar na mesa para participar do jogo
      A estratégia descrita está mais para implementar os recursos básicos indispensáveis, mas não se aprofundar nas extensões “comuns” que vêm depois. É uma forma de fazer com que recursos interessantes gerem um novo contato, ao mesmo tempo em que você tem funcionalidades básicas suficientes para não ser eliminado por faltar algum requisito essencial
    • Se entendi direito, a ideia é implementar o mínimo que os clientes corporativos precisam — ou seja, apenas o que a liderança deles exige para aprovar a negociação —, esquecer o resto e focar em algo realmente novo que diferencie você dos concorrentes?
    • Parecido com quando o iPhone saiu pela primeira vez sem copiar e colar, e com desempenho de rádio GSM muito decepcionante
  • Sinto algo parecido em relação a todo código wrapper. Às vezes alguém diz: “precisamos de uma versão interna desta API”.
    Os motivos variam, mas costumam ser algo como “não dá para confiar que vão usar a API oficial corretamente”. Pode até ser, mas a versão interna é menos padronizada e tem documentação pior.
    Às vezes o motivo é “precisamos de funcionalidades extras”; nesse caso, não é preciso envolver a API inteira, basta adicionar três funções. Com o tempo, 99% da base de código pode virar um polyfill.
    O ponto é: se você não usa o padrão, causa uma dor enorme para quem herdar a base de código depois.

    • Acho que, nesses wrappers, ajuda haver uma interface que permita interagir diretamente com a biblioteca de baixo nível.
      Por exemplo, a biblioteca ziggy-pydust, que permite escrever módulos nativos de Python em Zig, é claramente mais agradável de ver do que um import comum de Python.h. Ainda assim, ela também tem um .ffi para acessar diretamente funções que ainda não foram implementadas nela, mas existem em Python.h.
      Sem essa opção, em geral acabo abandonando esse tipo de biblioteca e preferindo a original. Dito isso, isso também é, em certo sentido, um wrapper — ou seja, um módulo nativo —, e às vezes, para ganhar velocidade de desenvolvimento, pode ser melhor usar ctypes diretamente.
  • É um bom texto e tem muitas ótimas lições, mas falta um ingrediente central. Isso é parecido com alternativas concorrentes a qualquer produto.
    É como dizer que a Amazon fracassou porque não tinha livrarias físicas às quais as pessoas estavam acostumadas, quando, na prática, não foi isso que aconteceu.
    O motivo pelo qual essas alternativas de JIT fracassaram e ficaram sempre correndo atrás é que, na prática, a maioria dos desenvolvedores da linguagem X não considera o JIT tão importante assim. Mais precisamente, eles consideram recursos da linguagem e interoperabilidade mais importantes do que JIT.
    Por isso, o produto que “se integra” em vez de competir vence. Porque ele não consegue oferecer maior estabilidade nem interoperabilidade.

  • Trabalho há muito tempo com linguagens e compiladores, e achei este texto muito interessante. Dizendo a mesma ideia de outro jeito: uma linguagem é muito mais do que simples velocidade de compilação.
    Velocidade de compilação é muito importante e certamente está entre as dez principais dimensões. Em especial, aumentá-la acelera o ciclo de feedback dos desenvolvedores, e a equipe principal consegue melhorar todas as outras dimensões mais rapidamente.
    Mesmo assim, há mais de 30 outras dimensões muito importantes em uma linguagem de programação.

    • O texto parece ser sobre velocidade de execução. Ainda assim, mesmo nesse caso, olhando para a popularidade do CPython, velocidade de execução claramente não é o fator número um.
  • A conclusão sobre como esse tipo de projeto pode ter sucesso é boa, mas ainda há um fator pouco mencionado sobre por que muitos projetos não decolam: a compatibilidade das implementações alternativas muitas vezes é menor, na prática, do que se afirma, inclusive para recursos antigos da linguagem.
    Por exemplo, é muito comum que apps em Ruby e Python tenham, em alguma dependência, uma extensão C nativa, e, até onde sei, as principais implementações alternativas nunca deram suporte a isso. Houve tentativas, mas por razões técnicas óbvias elas não funcionaram bem, e a alternativa de esperar que as bibliotecas oferecessem várias implementações também não teve uma história tranquila.
    Some a isso o fato de que essas linguagens são frequentemente usadas em sites CRUD, nos quais E/S pesa mais no desempenho do que CPU, e o apelo de uma alternativa mais rápida diminui bastante.

  • Texto realmente excelente. A sociologia da tecnologia é muito interessante.
    Mantenedores de implementações de linguagens querem o máximo de flexibilidade ao projetar e lançar novos recursos benéficos para os usuários. Eles não querem ficar travados por precisar obter consenso de várias implementações antes de disponibilizar um recurso. Como exemplo, basta ver como a evolução do JavaScript pareceu glacial por anos, e como o TypeScript evolui relativamente rápido.
    Ao mesmo tempo, implementações alternativas também têm vantagens, pois podem ser um sinal de que o ecossistema da linguagem é robusto. Se uma implementação alternativa for realmente boa, ela pode agregar valor real ao ecossistema, mesmo que só seja boa para alguns usuários com necessidades de nicho.
    Portanto, se você é designer ou mantenedor de uma linguagem, provavelmente não será ativamente hostil a implementações alternativas, mas há desvantagens. Em geral, o feedback dos usuários virá na direção de lançar novos recursos e fazer a linguagem evoluir. Não haverá muitos pedidos para desacelerar a fim de que PyPy, IronRuby, LuaJIT etc. consigam acompanhar.
    Quando consumidores de uma linguagem escolhem sobre qual implementação construir, as maiores prioridades normalmente são segurança e estabilidade. Ninguém quer que uma base de código de um milhão de linhas passe a depender de características comportamentais sutis de uma implementação alternativa que um dia foi criada por um doutorando muito inteligente, mas que agora partiu para outro projeto. Por isso, os usuários se concentram na implementação mais usada, e esse fato, por sua vez, atrai outros usuários, criando um forte loop de feedback positivo.
    Como resultado, a menos que haja uma força forte empurrando na direção contrária, a maioria das linguagens converge para uma única implementação de referência. É possível argumentar que isso é bom, porque quase todo o esforço de engenharia aplicado à implementação da linguagem beneficia todos os usuários, em vez de se dividir entre várias implementações. Claro, a desvantagem é que a implementação pode ficar presa em um ótimo local.

  • LuaJIT foi mencionado, mas também é um exemplo de que as coisas nem sempre acontecem como a conclusão do texto sugere. Muitas pessoas e projetos escolheram deliberadamente o LuaJIT em vez de Lua.

    • Vim aos comentários para dizer isso. LuaJIT é usado com entusiasmo pela comunidade TeX, e o executável luajittex também é facilmente encontrado ao lado de luatex.
  • Pode ser uma opinião menos popular, mas as pessoas precisam, às vezes, verificar o próprio ego, inclusive elas mesmas. Pode ser mais fácil criar um projeto paralelo que seja “meu” do que contribuir para um projeto open source existente, mas é preciso perguntar para quem você está fazendo isso
    É para os mantenedores do projeto, para o projeto em si, para os usuários ou para o próprio ego? Se o último item te irrita, provavelmente há alguma relação
    Adicionar JIT a uma linguagem existente é um trabalho grande, então a implementação de referência provavelmente terá critérios altos para aceitar isso. Ainda assim, acho que esse deveria ser o objetivo. Fazer um fork ou criar algo novo também dá a liberdade de fazer grandes coisas, mas muitas vezes deve ser visto como algo temporário
    Se o objetivo é mostrar do que eu sou capaz, é provável que não vá muito longe. Se o objetivo é criar algo melhor, você aprende a trabalhar dentro das restrições de outras pessoas

    • Surpreendentemente, penso o contrário. Quem prega que deve haver um único projeto, contribuições apenas para o projeto de referência e nenhuma implementação alternativa é que deveria verificar o próprio ego e aprender que a existência de várias abordagens e a concorrência são melhores do que o monopólio
      Acho que este texto mostra como Python, Lua e Ruby optaram por essa abordagem e decepcionaram muita gente. Como resultado, milhares de desenvolvedores e milhões de usuários acabam tolerando desenvolvimento e software mais lentos. Não porque seja impossível, mas porque, administrativamente, não há incentivo para agir diferente
    • Hmm, há casos em que a implementação oficial é completamente amaldiçoada, então às vezes é preciso criar o seu próprio jardinzinho. Mesmo que ele cresça para 20 acres com um plano de rotação de culturas, às vezes ainda é melhor do que um vulcão