2 points par GN⁺ 2024-07-31 | 1 commentaires | Partager sur WhatsApp
  • Porffor est un projet de recherche qui compile JavaScript à l’avance, plutôt qu’au moment de l’exécution, pour produire du WebAssembly et des binaires natifs
  • Grâce à une approche qui n’embarque pas d’interpréteur, il vise des sorties 10 à 30 fois plus petites et plus rapides que les projets JS→Wasm existants
  • Pour les builds natifs également, comme il ne package pas de runtime, la taille des binaires peut diminuer jusqu’à 1000 fois ; l’exemple passe d’environ 90 Mo à moins de 100 Ko
  • Il est écrit en JS, n’utilise pas eval, et met en avant une architecture avec prise en charge native de TypeScript sans étape de build séparée
  • L’AOT facilite les optimisations par analyse statique et la compilation avant exécution, mais rend difficile l’évaluation dynamique de JS comme eval, et le projet en est encore à un stade précoce où beaucoup de code JS ne fonctionne pas encore

Le mode d’exécution que crée Porffor

  • Porffor est un projet de recherche qui compile JavaScript en Ahead-of-Time vers WebAssembly et des binaires natifs
  • Le binaire TypeScript compilé avec Porffor sert cette page
  • Conçu dès le départ pour l’AOT, il tente des optimisations difficiles à réaliser avec les modes d’exécution JS classiques

Différences entre les sorties WebAssembly et natives

  • JS → Wasm

    • La sortie WebAssembly de Porffor est 10 à 30 fois plus petite et plus rapide que celles des projets JS→Wasm existants
    • La différence clé est qu’il compile directement le JS, sans bundler d’interpréteur
    • Exécuter du JS dans Wasm permet une exécution en sandbox, mais peut entraîner une forte perte de performances ; Porffor se concentre sur la réduction de ce coût
    • Cas d’usage possibles :
      • Hébergement JS côté serveur : dans les edge runtimes, le sandboxing Wasm peut offrir une exécution sûre sans isolation excessive
      • Le faible overhead de l’AOT ouvre la possibilité d’exécuter davantage de clients sur le même matériel qu’avec un JIT, avec une perte de performances minimale
      • Résistance au reverse engineering : pour du JS sensible, du code compilé peut être plus difficile à rétroconcevoir que du code obfusqué
  • JS → Native

    • Comme le JS est réellement compilé sans packager de runtime, la taille des binaires peut être jusqu’à 1000 fois plus petite
    • La taille d’exemple est d’environ 90 Mo → moins de 100 Ko
    • En interne, le JS est compilé en C puis en natif ; ainsi, partout où l’on peut utiliser C, on peut utiliser JS
    • Cas d’usage possibles :
      • Exécution de JS rapide dans l’embarqué, les consoles de jeu, etc.
      • Petites apps CLI en JS compilées en exécutables one-click de moins de 1 Mo

Avantages et limites de l’AOT

  • Les interpréteurs traditionnels et les multiples niveaux de JIT doivent trouver un équilibre entre temps de démarrage et performances JS
  • Avec l’AOT, on compile d’abord et on exécute ensuite ; la vitesse de compilation est donc importante pour l’expérience développeur, mais n’affecte pas l’expérience utilisateur
  • Cette approche laisse de la place à des optimisations fondées sur l’analyse statique, comme en C++ et en Rust
  • Les principaux inconvénients sont l’absence d’évaluation dynamique de JS comme eval, et la nécessité de créer un nouveau moteur JS
  • Le projet en est encore à ses débuts, donc beaucoup de code JS ne fonctionne pas encore, mais des améliorations sont en cours
  • Pour suivre l’avancement de la compatibilité ECMAScript, la suite de tests officielle Test262 est exécutée à chaque commit

1 commentaires

 
GN⁺ 2024-07-31
Commentaires sur Hacker News
  • Oliver, le principal développeur de Porffor, a annoncé qu’il travaillerait à plein temps sur Porffor : https://x.com/canadahonk/status/1818347311417938237

  • J’ai pensé à quelque chose de similaire, mais il me semble difficile d’obtenir de bien meilleures performances en JavaScript. Au mieux, ce serait sans doute une sorte de transpilation de JS vers des appels C++ de V8
    Les optimisations vraiment intéressantes apparaissent lorsqu’on compile du TypeScript ou quelque chose de proche. Exploiter les types peut apporter un gros gain, et les parties non typées retomberaient par défaut sur des appels JS lents. Les interfaces pourraient être réduites à des tables de fonctions virtuelles ou à des appels directs, et on pourrait aussi travailler sur des structures plutôt que sur des maps. On pourrait avoir des types Int et Float, rétrogradés en Number si nécessaire, tout en les conservant dans les registres
    Le problème central, c’est que TS et V8 sont tous deux des cibles non standard qui évoluent vite. Ce genre de projet nécessite une grande équipe, et le simple maintien de la compatibilité devient un travail à part entière

    • Sans extensions supplémentaires, TypeScript aide moins qu’on pourrait le croire. Il n’a pas été conçu à l’origine pour cet usage
      Exemple simple : TypeScript ne distingue pas les entiers des nombres à virgule flottante, il traite tout comme des nombres. Du coup, chaque accès à un tableau nécessite une conversion de type. Si TypeScript avait été conçu pour faciliter la compilation statique, cette distinction existerait probablement
      Le problème plus important, c’est le sous-typage structurel de TypeScript. À cause de cette propriété, il est pratiquement impossible pour le compilateur de déterminer statiquement la structure physique des arguments non primitifs passés à une fonction. Un JIT peut effectuer une analyse dynamique des formes, donc on risque d’obtenir des performances inférieures au JIT sur chaque accès à un champ
    • En tant que contributeur à Porffor, je ne suis pas d’accord. Il reste encore pas mal de marge pour améliorer JavaScript au moment de la compilation
      De nombreux travaux ont déjà porté sur des outils d’analyse statique de types pour JS, et il est possible de faire des analyses très poussées. Un exemple qui me vient à l’esprit est TAJS, même si c’est un peu ancien
    • Un projet assez lié à cette idée est AssemblyScript : https://www.assemblyscript.org
    • ECMAScript 4 était une tentative d’ajouter de meilleurs types au langage, mais cela a malheureusement échoué il y a longtemps
      J’aimerais au moins que TypeScript permette de spécifier des types comme integer. Même si une compilation TS→JS classique traite const val: int exactement comme const val: number, un runtime moderne capable de comprendre TS pourrait exploiter cette information supplémentaire
      Je me demande si une syntaxe comme const counter: Number pourrait être acceptée
    • Dire « j’y ai pensé, mais il est difficile d’obtenir de meilleures performances », puis proposer une approche qui exige précisément ce qui est expliqué tout en haut de la page d’accueil du site, c’est assez étrange
      Je ne sais pas si le site a changé, ou si c’est moi qui ai raté quelque chose
  • Chez windmill.dev, quand les utilisateurs déploient du code, ils utilisent Bun build pour regrouper le script et toutes ses dépendances dans un seul fichier JS, qu’ils chargent ensuite afin d’améliorer le cold start et l’utilisation mémoire. À cause de la taille du bundle, le résultat est stocké sur S3
    Si tout pouvait être bundlé nativement, ce serait complètement différent. Même si le cold start de Bun est excellent, il est difficile de rivaliser avec une exécution native directe depuis un petit binaire

    • En tant que développeur, je suis d’accord. Cela semble être un cas d’usage intéressant où Porffor pourrait potentiellement aider. Ce serait bien d’en discuter un jour
  • J’aime voir davantage de runtimes JS explorer l’accès à Wasm. Ce projet me rappelle Static Hermes, le moteur JS de Facebook destiné à accélérer les performances sur iOS et Android pour les projets React Native
    Les deux visent la conformité à JS test262, et Porffor prend en charge à la fois la sortie native et Wasm, tandis que Static Hermes se concentre actuellement surtout sur la sortie native. Porffor est écrit en pur JS et vise à pouvoir se compiler lui-même, alors que Static Hermes dépend de LLVM. La prise en charge de async/promise/await restait encore limitée dans Porffor, tandis que Static Hermes la prend en charge avec certaines restrictions. Static Hermes est écrit en C++, Porffor principalement en JS. Les deux prennent en charge TypeScript, mais Static Hermes transpile l’AST TS vers Flow, alors que Porffor le gère nativement. Static Hermes dispose d’un interpréteur de secours pour les situations JS difficiles à compiler comme eval, tandis que Porffor ne prend en charge que la compilation anticipée
    Globalement, j’espère que ce projet gagnera en élan et pourra rendre le moteur JavaScript de l’edge plus rapide. Signé Syrus de Wasmer
    https://github.com/facebook/hermes/discussions/1137
    https://github.com/tc39/test262
    https://wasmer.io

    • Pour information, Static Hermes prend entièrement en charge la compilation de JS vers WASM. C’est quasiment obtenu gratuitement grâce au backend LLVM existant. On peut voir un exemple ici : https://x.com/tmikov/status/1706138872412074204
      Cela dit, ce n’est pas notre priorité, nous nous concentrons surtout sur React Native. Dans cet environnement, WASM n’a pas vraiment d’intérêt
      La fonctionnalité la plus importante de Static Hermes est un vérificateur de types qui garantit la solidité à l’exécution. Porffor est très intéressant, je le suis depuis un moment et j’espère qu’il réussira
    • En tant que contributeur à Porffor, je trouve que c’est une bonne comparaison. Cela dit, Porffor prend aussi en charge les promesses sur le plan technique. Elles fonctionnent simplement de manière synchrone
      C’est une approche similaire à Kiesel : https://kiesel.dev/
    • Quelques petites corrections. Porffor n’est pas encore totalement self-hosted, mais on pense que ce sera possible. Cela dit, des fonctionnalités intégrées comme Array.prototype.filter, Math.sin et atob compilent déjà en partie elles-mêmes
      Récemment, Porffor a aussi commencé à prendre en charge les bases de async/promise/await. Cela ne fonctionne pas encore très bien
    • On dirait que tu présentes la dépendance à LLVM comme quelque chose de négatif
  • Il existe dans JavaScript un sous-ensemble facile à compiler, et la difficulté se trouve dans la longue traîne qui reste. Cela dit, c’est formidable de voir des recherches menées sur où se situe cette frontière et sur les gains qu’on peut tirer de ce sous-ensemble

  • J’aime sincèrement le fait qu’il prenne en charge String.blink. Le fait qu’un développeur ait de l’humour et un côté joueur est toujours bon signe

    • Si l’objectif est qu’un “hôte ECMAScript se comporte comme un navigateur web”, alors il faut évidemment le prendre en charge. Cela fait partie de la spécification : https://tc39.es/ecma262/multipage/additional-ecmascript-feat...
      L’implémentation elle-même est aussi triviale que function() { return "" + this + ""; }, donc cela vaut la peine de l’implémenter même si l’hôte ECMAScript n’est pas un navigateur web. Dans ce cas, c’est facultatif. Je ne m’attendrais pas à ce que cela ait quoi que ce soit à voir avec de “l’humour ou un côté joueur”
    • String.blink figure dans test262, donc si l’on veut atteindre les objectifs du projet, il faut en pratique le prendre en charge
  • Je me demande quelle nuance subtile m’a échappé. Je ne vois pas pourquoi “moteur JS en compilation anticipée” serait une meilleure description que “compilateur JS-vers-Wasm”. Si c’est surtout une stratégie de cadrage, cela me va aussi

    • Il existe déjà des projets qui embarquent un interpréteur JS pour faire du JS-to-WASM. C’est donc probablement une façon d’exprimer plus clairement la différence avec ce type d’approche
  • Le système de versioning décrit ici me semble un peu douteux
    Si un changement provoque une régression sur certains tests Test262, alors le numéro de version pourrait lui aussi devoir reculer. Autrement dit, Porffor ne peut pas avoir à la fois des numéros de version monotoniquement croissants et la possibilité d’introduire des changements nécessaires qui causent des régressions Test262
    https://github.com/CanadaHonk/porffor?tab=readme-ov-file#ver...

    • L’idée est probablement que les travaux provoquant des régressions Test262 sont temporaires et effectués sur une branche séparée, puis ne sont fusionnés dans main qu’une fois toutes les corrections nécessaires pour supprimer ces régressions incluses. Il suffit alors de n’utiliser un nouveau numéro de version qu’après cette fusion
  • Cela signifie “violet” en gallois

    • L’étymologie vient du grec pour violet, et le mot anglais le plus courant partageant la même racine est probablement porphyry, un minéral violet
  • C’est rafraîchissant de voir plusieurs moteurs JS apparaître pour des usages variés
    Pour embarquer des plugins dans une application, j’ai travaillé à fournir davantage d’API compatibles Node à quickjs via llrt
    https://github.com/awslabs/llrt