2 pontos por GN⁺ 2024-07-31 | 1 comentários | Compartilhar no WhatsApp
  • Porffor é um projeto de pesquisa que compila JavaScript antecipadamente, em vez de no momento da execução, gerando WebAssembly e binários nativos
  • Graças à abordagem de não empacotar um interpretador junto, ele busca produzir saídas 10 a 30 vezes menores e mais rápidas do que projetos JS→Wasm existentes
  • Mesmo em builds nativos, ele não empacota um runtime, então o tamanho do binário pode cair em até 1000 vezes; em um exemplo, diminui de cerca de 90 MB para menos de 100 KB
  • É escrito em JS, não tem eval e destaca uma arquitetura com suporte nativo a TypeScript sem etapa de build separada
  • AOT favorece otimizações por análise estática e compilação antes da execução, mas dificulta avaliação dinâmica de JS como eval e ainda está em fase inicial, com muito código JS ainda sem funcionar

Como o Porffor executa código

  • Porffor é um projeto de pesquisa que compila JavaScript Ahead-of-Time para WebAssembly e binários nativos
  • Um binário TypeScript compilado com Porffor serve esta página
  • Ele foi escrito desde o início pensando em AOT, com uma estrutura que tenta fazer otimizações difíceis nas formas tradicionais de execução de JS

Diferença entre saída WebAssembly e nativa

  • JS → Wasm

    • A saída WebAssembly do Porffor é 10 a 30 vezes menor e mais rápida do que a de projetos JS→Wasm existentes
    • A principal diferença é que ele compila JS diretamente e não empacota um interpretador
    • Executar JS em Wasm permite execução em sandbox, mas pode trazer grande perda de desempenho; o foco do Porffor é reduzir esse custo
    • Casos de uso possíveis:
      • Hospedagem de JS no lado do servidor: em runtimes de edge, o sandboxing com Wasm pode oferecer execução segura sem isolamento excessivo
      • O baixo overhead do AOT, em comparação com JIT, pode permitir executar mais clientes no mesmo hardware com perda mínima de desempenho
      • Resistência à engenharia reversa: JS sensível pode ser mais difícil de reverter em código compilado do que com ofuscação
  • JS → Native

    • Como ele realmente compila JS sem empacotar um runtime, o tamanho do binário pode ficar até 1000 vezes menor
    • Um exemplo de tamanho é cerca de 90 MB → menos de 100 KB
    • Internamente, ele compila JS para C e depois compila para nativo, então onde for possível usar C, será possível usar JS
    • Casos de uso possíveis:
      • Execução de JS rápido em embarcados, consoles de jogos etc.
      • Pequenos apps CLI em JS compilados como executáveis de um clique com menos de 1 MB

Vantagens e limitações do AOT

  • Interpretadores tradicionais ou múltiplas etapas de JIT precisam equilibrar tempo de inicialização e desempenho de JS
  • No AOT, compila-se primeiro e executa-se depois, então a velocidade de compilação é importante para a experiência do desenvolvedor, mas não afeta a experiência do usuário
  • Essa abordagem abre espaço para otimizações baseadas em análise estática como em C++ e Rust
  • As principais desvantagens são a ausência de avaliação dinâmica de JS como eval e a necessidade de criar um novo motor JS
  • Como ainda está em estágio inicial, muito código JS ainda não funciona, mas melhorias estão em andamento
  • Para acompanhar o progresso de compatibilidade com ECMAScript, ele executa a suíte oficial de testes Test262 a cada commit

1 comentários

 
GN⁺ 2024-07-31
Opiniões no Hacker News
  • Oliver, principal desenvolvedor do Porffor, anunciou que vai trabalhar em tempo integral no Porffor: https://x.com/canadahonk/status/1818347311417938237

  • Já pensei em algo parecido, mas acho difícil obter desempenho muito melhor em JavaScript. Talvez o melhor seja transpilar JS para chamadas C++ do V8
    As otimizações realmente interessantes aparecem ao compilar TypeScript, ou algo próximo disso. Usar tipos pode trazer grandes ganhos, e as partes sem tipos basicamente cairiam para chamadas JS lentas. Interfaces poderiam ser reduzidas a tabelas de funções virtuais ou chamadas diretas, e também seria possível operar sobre structs em vez de mapas. Também daria para ter tipos Int e Float e rebaixá-los para Number quando necessário, mantendo-os em registradores
    O problema central é que tanto TS quanto V8 são alvos não padronizados que mudam rapidamente. Um projeto assim só é viável com uma equipe grande, e manter a compatibilidade vira um trabalho à parte

    • Sem extensões adicionais, TypeScript ajuda menos do que parece. Isso porque ele não foi projetado para esse uso desde o início
      Um exemplo simples: TypeScript não distingue inteiros de números de ponto flutuante; trata tudo como números. Por isso, todo acesso a arrays precisa de conversão de tipo. Se o TypeScript tivesse sido projetado para ajudar na compilação estática, provavelmente teria essa distinção
      O problema maior é a subtipagem estrutural do TypeScript. Por causa dessa característica, é praticamente impossível para o compilador determinar estaticamente a estrutura física de argumentos não primitivos passados a uma função. Um JIT consegue fazer análise dinâmica de formas, então o desempenho pode ficar pior que o de um JIT em todos os acessos a campos
    • Como contribuidor do Porffor, discordo. JavaScript também tem bastante espaço para melhorias em tempo de compilação
      Já houve muito trabalho em ferramentas de análise estática de tipos para JS, e análises bastante rigorosas também são possíveis. Um exemplo que me vem à cabeça, embora meio antigo, é o TAJS
    • Um projeto um pouco relacionado a essa ideia é o AssemblyScript: https://www.assemblyscript.org
    • O ECMAScript 4 foi uma tentativa de adicionar tipos melhores à linguagem, mas infelizmente fracassou há muito tempo
      Seria bom se até o TypeScript permitisse especificar tipos como integer. Mesmo que, na compilação TS→JS comum, const val: int fosse tratado exatamente igual a const val: number, runtimes modernos que entendem TS poderiam aproveitar essa informação adicional
      Fico pensando se uma sintaxe como const counter: Number poderia ser aceita
    • Depois de dizer “pensei nisso, mas é difícil obter desempenho melhor”, está falando de uma abordagem que pede coisas explicadas logo no topo da página inicial do site
      Não sei se o site mudou ou se estou deixando passar alguma coisa
  • No windmill.dev, quando usuários fazem deploy de código, usamos o Bun build para empacotar o script e todas as dependências em um único arquivo JS, e então o carregamos para melhorar o cold start e o uso de memória. Por causa do tamanho do bundle, o resultado é armazenado no S3
    Se fosse possível empacotar tudo como nativo, o jogo mudaria completamente. Por melhor que seja o cold start do Bun, é difícil vencer a execução nativa direta a partir de um binário pequeno

    • Como desenvolvedor, concordo. Parece um caso de uso interessante em que o Porffor poderia potencialmente ajudar. Seria bom conversarmos algum dia
  • É bom ver mais runtimes JS abordando Wasm. Este projeto me lembra o Static Hermes, o mecanismo JS do Facebook para acelerar projetos React Native no iOS e Android
    Ambos têm como objetivo a conformidade com o JS test262; enquanto o Porffor oferece suporte tanto à saída nativa quanto a Wasm, o Static Hermes atualmente se concentra principalmente em saída nativa. O Porffor é escrito em JS puro e caminha para conseguir compilar a si mesmo, enquanto o Static Hermes depende do LLVM. O Porffor ainda tinha suporte limitado a async/promise/await, e o Static Hermes oferece suporte com algumas limitações. O Static Hermes é escrito em C++, e o Porffor principalmente em JS. Ambos oferecem suporte a TypeScript, mas o Static Hermes transpila a AST de TS para Flow, enquanto o Porffor oferece suporte nativo. O Static Hermes tem um interpretador de fallback para casos de JS difíceis de compilar, como eval, enquanto o Porffor oferece apenas compilação antecipada
    No geral, estou ansioso para ver se este projeto ganha tração e consegue tornar os mecanismos JavaScript na edge mais rápidos. Deixado por Syrus, da Wasmer
    https://github.com/facebook/hermes/discussions/1137
    https://github.com/tc39/test262
    https://wasmer.io

    • Só para constar, o Static Hermes tem suporte completo à funcionalidade de compilar JS para WASM. Como já existe um backend LLVM, é um recurso que sai praticamente de graça. Para um exemplo, veja https://x.com/tmikov/status/1706138872412074204
      Dito isso, esse não é o nosso foco; estamos concentrados principalmente em React Native. Nesse ambiente, WASM não faz muito sentido
      O recurso mais importante do Static Hermes é o verificador de tipos, que garante a solidez em tempo de execução. O Porffor é muito interessante, venho acompanhando há algum tempo e estou torcendo para que dê certo
    • Como colaborador do Porffor, acho que é uma boa comparação. Dito isso, o Porffor também oferece suporte a promises tecnicamente. Elas só funcionam de forma síncrona
      É uma abordagem parecida com a do Kiesel: https://kiesel.dev/
    • Há algumas pequenas correções. O Porffor ainda não é totalmente self-hosted, mas esperamos que isso seja possível. Porém, funcionalidades embutidas como Array.prototype.filter, Math.sin e atob já compilam parcialmente a si mesmas
      Recentemente, o Porffor também começou a oferecer suporte básico a async/promise/await. Ainda não funciona tão bem
    • Parece que você falou da dependência do LLVM como se isso fosse algo ruim
  • JavaScript tem um subconjunto que pode ser compilado facilmente, e o que é difícil é a longa cauda fora dele. Ainda assim, é ótimo ver pesquisas avançando sobre onde fica essa fronteira e quanto ganho é possível obter nesse subconjunto

  • Gosto sinceramente do fato de oferecer suporte a String.blink. Um desenvolvedor ter humor e espírito brincalhão é sempre um bom sinal

    • Se a ideia é que um “host ECMAScript se comporte como um navegador web”, então é claro que deve oferecer suporte. Isso faz parte da especificação: https://tc39.es/ecma262/multipage/additional-ecmascript-feat...
      A implementação também é trivial, algo no nível de function() { return "" + this + ""; }, então vale a pena implementar mesmo que o host ECMAScript não seja um navegador web. Nesses casos, é opcional. Eu não esperaria que isso tivesse relação com “humor ou espírito brincalhão”
    • String.blink está no test262, então, para cumprir os objetivos do projeto, na prática é preciso oferecer suporte a ele
  • Fico curioso sobre qual nuance sutil deixei passar. Não entendo por que “mecanismo JS de compilação antecipada” é uma descrição melhor do que “compilador de JS para Wasm”. Se for principalmente uma estratégia de enquadramento, tudo bem também

    • Já existem projetos que fazem JS-to-WASM empacotando um interpretador JS. Então é provável que a expressão tente deixar mais clara a diferença em relação a essa abordagem
  • O esquema de versionamento descrito aqui me parece um pouco suspeito
    Se alguma alteração causar regressão em alguns testes do Test262, o número de versão também poderia voltar atrás. Ou seja, o Porffor não pode ter ao mesmo tempo números de versão monotonicamente crescentes e a capacidade de uma mudança necessária causar regressões no Test262
    https://github.com/CanadaHonk/porffor?tab=readme-ov-file#ver...

    • Talvez a intenção seja que trabalhos que causem regressões no Test262 sejam temporários e feitos em um branch separado, sendo mesclados ao main apenas quando também incluírem todas as correções necessárias para eliminar as regressões. O novo número de versão só precisaria ser usado depois desse merge
  • Em galês, significa “roxo

    • A etimologia vem do grego para roxo, e a palavra em inglês com a mesma raiz provavelmente mais comum é porphyry, um mineral roxo
  • É revigorante ver vários mecanismos JS surgindo para usos diferentes
    Tenho trabalhado, via llrt, para oferecer mais APIs compatíveis com Node ao quickjs, com o objetivo de embutir plugins em uma aplicação
    https://github.com/awslabs/llrt