2 points par GN⁺ 2024-05-13 | 1 commentaires | Partager sur WhatsApp
  • Les implémentations alternatives comme les runtimes de langage ou les JIT peuvent voir leur adoption limitée, même avec de bonnes performances, car elles doivent continuer à suivre les évolutions de l’implémentation canonique et les attentes des utilisateurs
  • PyPy, LuaJIT et TruffleRuby ont montré de bonnes performances d’exécution, mais le décalage de compatibilité et la charge liée au suivi des nouvelles fonctionnalités constituent un frein réel au déploiement
  • YJIT a choisi d’être intégré à l’intérieur de CRuby plutôt que d’être une implémentation Ruby séparée, afin d’être compatible à 100 % avec les fonctionnalités de CRuby dès le départ, et il est déployé chez Shopify, Discourse et GitHub
  • Un choix comme Crystal, très proche d’un langage existant sans être compatible avec lui, peut amener les utilisateurs à se heurter sans cesse à des différences de type « presque Ruby, mais pas Ruby »
  • Dans des domaines où il existe un standard public séparé des implémentations, comme les parseurs JSON ou JavaScript, la charge des implémentations alternatives diminue, mais dans les écosystèmes où l’implémentation canonique fait de facto office de standard, une autre stratégie est nécessaire

Le piège récurrent des implémentations alternatives

  • Dans le monde du logiciel, il arrive régulièrement que des projets lancés comme meilleure implémentation alternative d’un système existant restent prisonniers de l’ombre de l’implémentation canonique
  • Une implémentation alternative est comparée à l’implémentation canonique (canonical implementation), perçue comme la référence en matière de fonctionnalités, de performances, d’écosystème et d’attentes des utilisateurs
  • Si l’implémentation canonique continue d’évoluer, l’implémentation alternative doit consacrer beaucoup d’énergie à suivre ces changements au lieu de définir sa propre direction
  • Lorsqu’on ajoute une implémentation JIT à un langage historiquement interprété, les nouvelles fonctionnalités ont tendance à arriver plus vite dans l’interpréteur, ce qui augmente la charge de suivi côté JIT

Le schéma observé avec PyPy, LuaJIT et TruffleRuby

  • PyPy est un compilateur JIT avancé pour Python, capable d’apporter de gros gains de vitesse par rapport à CPython, mais son usage réel est resté très limité
    • Python est une cible mouvante, avec de nouvelles versions et fonctionnalités de CPython qui arrivent régulièrement
    • PyPy a eu du mal à suivre, restant en permanence en retard de plusieurs versions de Python
    • Rendre un logiciel Python compatible avec PyPy impose de limiter les fonctionnalités Python utilisables, et la plupart des développeurs Python ne veulent pas s’en préoccuper
  • LuaJIT offre un gain de performances important par rapport à l’implémentation Lua classique basée sur un interpréteur, et a reçu de très bonnes évaluations ainsi qu’un certain niveau d’adoption réelle
    • Son créateur, Mike Pall, est considéré par beaucoup comme un programmeur exceptionnel
    • Comme le langage Lua a continué à ajouter de nouvelles fonctionnalités, LuaJIT s’est lui aussi retrouvé en retard de plusieurs versions
    • Certains utilisateurs de Lua hésitent à utiliser LuaJIT pour cette raison
    • Lua est connu pour son minimalisme, mais aucun effort n’a été fait pour ralentir l’ajout de nouvelles fonctionnalités ou se coordonner avec Mike Pall
  • TruffleRuby a affiché parmi les chiffres de performance les plus impressionnants des JIT Ruby, mais son déploiement est resté limité
    • Une des raisons pratiques est que le temps de chauffe de TruffleRuby est bien plus long que celui de CRuby
    • À mesure que CRuby ajoutait de nouvelles fonctionnalités, les contributeurs de TruffleRuby devaient faire des efforts pour suivre
    • Les utilisateurs de Ruby considèrent CRuby comme l’implémentation canonique, et jugent qu’une implémentation non totalement compatible présente peu d’intérêt

La voie différente choisie par YJIT

  • YJIT a commencé comme un JIT Ruby de plus, mais a été construit à l’intérieur même de CRuby plutôt que comme une implémentation séparée
  • Ce choix a créé plusieurs compromis de conception, mais il a permis à YJIT d’être compatible à 100 % avec toutes les fonctionnalités de CRuby dès le départ
  • Aujourd’hui, YJIT est le JIT « officiel » de Ruby et il est déployé chez Shopify, Discourse et GitHub
  • Les utilisateurs qui visitent github.com ou une boutique Shopify interagissent donc, en pratique, avec YJIT
  • Jusqu’à présent, YJIT est le compilateur JIT Ruby qui a connu le plus grand succès, et la compatibilité a joué un rôle central dans ce succès

« If you can’t beat them, join them » ne suffit pas

  • Se positionner comme implémentation alternative conduit souvent à rester dans l’ombre de l’implémentation canonique, dans une course permanente au rattrapage
  • Si le projet canonique continue d’évoluer, l’implémentation alternative doit suivre avec une capacité limitée à décider de sa propre trajectoire
  • Rejoindre l’implémentation canonique peut produire de meilleurs résultats, mais cela ne résout pas tous les cas
  • Dans l’écosystème Ruby, Crystal est un langage compilé statiquement avec une syntaxe proche de Ruby et utilisant l’inférence de types
    • Crystal ne vise délibérément pas la compatibilité avec Ruby et s’en est détaché
    • Pour les Rubyistes, il apparaît comme un langage « presque Ruby, mais pas vraiment Ruby », avec en pratique de nombreuses différences subtiles et incompatibilités
    • Cette ressemblance brouille les attentes des utilisateurs et crée de la confusion
    • Crystal aurait peut-être obtenu de meilleurs résultats s’il ne s’était pas présenté dès le départ comme proche de Ruby

Éviter la concurrence et suivre sa propre direction

  • Le « competition is for losers » de Peter Thiel est utilisé ici dans le sens de ne pas se placer soi-même dans une situation de concurrence inutile
  • Cela mène au conseil suivant : si l’on crée un nouveau langage de programmation, mieux vaut éviter d’en faire un sous-ensemble de Python ou quelque chose de très proche en surface d’un langage existant
  • En créant quelque chose de vraiment à soi, on peut faire évoluer son système à son propre rythme et dans sa propre direction, sans être prisonnier des attentes liées aux performances, au jeu de fonctionnalités ou à l’écosystème de bibliothèques d’une autre implémentation
  • Ce conseil s’applique dans les situations où il existe une implémentation canonique pour un langage ou un système
  • Les domaines disposant d’un standard public peuvent constituer une exception
    • Les parseurs JSON reposent sur une spécification claire, relativement petite et qui évolue peu, ce qui permet de développer sa propre implémentation
    • JavaScript possède plusieurs implémentations basées sur les navigateurs, ce qui est possible grâce à l’existence d’un organisme de standardisation externe qui gère la spécification JS
    • Les responsables du standard JS comprennent que les implémentations avec compilation JIT sont cruciales pour les performances, et font évoluer le langage en conséquence
    • Leur objectif n’est pas d’ajouter le plus vite possible un grand nombre de nouvelles fonctionnalités

1 commentaires

 
GN⁺ 2024-05-13
Avis sur Hacker News
  • Il y a un autre point important que l’OP a manqué. Quand on crée une implémentation alternative, son architecture est généralement différente de celle de l’implémentation de référence, et quelque chose de facile dans l’implémentation de référence peut être très difficile dans la vôtre.
    Imaginons par exemple qu’un logiciel propriétaire de reporting financier stocke ses documents dans un format binaire étrange. En créant une alternative gratuite, vous choisissez une architecture qui lit tout le document en mémoire et réécrit tout le fichier à l’enregistrement ; on peut supposer que l’original, lui, a été conçu à une époque où la RAM était limitée, ne lit et n’écrit que la section sur laquelle l’utilisateur travaille, et permet même des modifications en place.
    Si, plus tard, l’original ajoute la possibilité d’insérer des pièces jointes dans les documents, même de gros fichiers comme l’enregistrement d’un appel investisseurs ou un PDF scanné de plusieurs centaines de pages fonctionnent bien grâce au chargement par sections. En revanche, votre implémentation désérialise tout le document : dès que celui-ci dépasse la RAM de l’utilisateur, c’est un problème, et une modification qu’un seul développeur pourrait terminer en une semaine dans l’original peut vous obliger à reconcevoir tout le logiciel.

    • Je travaille dans un secteur où chaque composant logiciel critique doit avoir deux implémentations indépendantes, et j’ai participé à plusieurs réimplémentations. Il m’est arrivé de voir des fonctionnalités qui étaient clairement liées au fait que l’implémentation d’origine faisait X, et qui s’intégraient mal à notre architecture. Malgré tout, aucune de ces fonctionnalités ne nous a jamais obligés à reconcevoir le programme ; c’était plutôt le genre de fonction triviale d’un côté, mais demandant un peu plus de travail de l’autre.
      À l’inverse, il y a aussi eu des fonctionnalités plus difficiles à implémenter dans l’original, mais triviales dans la seconde implémentation. La seule grande refonte que j’ai vue concernait l’implémentation d’origine elle-même, qui reposait sur une hypothèse entraînant une explosion exponentielle.
      Un exemple public est celui de l’exécution d’applications Windows sous Linux. Le noyau Linux est une implémentation complètement différente de NT et ne vise même pas la compatibilité, mais exécuter des applis Windows ne nécessite pas de refondre tout le noyau. Quelques fonctionnalités de noyau assez générales et une couche de compatibilité en espace utilisateur suffisent. Wine demande beaucoup d’efforts à écrire et à maintenir, mais bien moins qu’une implémentation de Windows lui-même, et il fonctionne sur une plateforme qui n’a jamais visé la compatibilité Windows. Cela dit, comme l’explique l’article, il faut sans cesse courir après Windows, et comme du code finit par dépendre des bugs de l’implémentation de référence, il faut d’abord découvrir quels bugs implémenter si l’on veut être compatible jusque dans les bugs.
    • L’auteur semble déjà couvrir ce point lorsqu’il explique que la difficulté d’implémenter de nouvelles fonctionnalités diffère entre langages interprétés et langages compilés.
    • Exact. C’est le problème classique de rendre Python rapide. CPython a commencé comme un simple interpréteur qui suit ce que le code lui demande sans optimisation ; tout y est dictionnaire et il n’y a presque pas de véritable concurrence.
      Ainsi, n’importe quel code peut modifier autre chose pendant l’exécution. En pratique, ce n’est pas si souvent utilisé, mais si l’on essaie de le retirer, les gens crient au scandale. Une implémentation qui compile réellement Python doit même gérer le cas où un thread change soudainement quelque chose sous les pieds d’un autre thread.
    • Du point de vue du dilemme de l’innovateur, l’implémentation de référence peut aussi être celle d’un concurrent. Si, maintenant que vous connaissez mieux le marché, vous choisissez une meilleure architecture, vous pouvez vous retrouver en position d’ajouter de nouvelles fonctionnalités moins cher et plus vite que lui.
      C’est l’une des façons dont une petite entreprise peut alourdir le fardeau d’une grande, et c’est aussi possible pour une entreprise qui a moins laissé traîner sa dette technique. C’est l’un des rares moments où l’on peut rendre la dette technique très visible pour la direction, puisqu’on peut dire : « il nous faut plus de temps qu’à Acme pour implémenter cette fonctionnalité ».
    • Ces différences d’architecture sont parfois intentionnelles. Par exemple, les versions GNU de nombreux utilitaires Unix de base ont choisi des compromis complètement différents de ceux des versions Unix et BSD originales afin d’éviter les soupçons de violation de copyright.
  • Je suis d’accord avec l’idée de « ne pas essayer de créer un sous-ensemble de Python ». Les projets commercialisés comme « Python, mais avec X en mieux » ont toujours du mal à rivaliser avec l’implémentation de référence, surtout si X est la vitesse. Les personnes qui utilisent des langages à typage dynamique finissent souvent par ne pas tant se soucier de la vitesse d’exécution.
    Mais les implémentations alternatives ne sont pas toujours vouées à l’échec. MicroPython semble avoir plutôt réussi, même s’il ne prend quasiment rien en charge au-delà de Python 3.4. C’est parce qu’il est conçu pour tourner sur des microcontrôleurs : il ne concurrence pas CPython, mais d’autres environnements de programmation pour microcontrôleurs.
    Cela dit, les mainteneurs de MicroPython doivent recevoir beaucoup de demandes pour des fonctionnalités Python plus récentes. À une époque, j’avais envisagé une implémentation alternative de Python légère et axée sur l’intégration dans des applications ; là encore, l’idée était de concurrencer Lua, pas CPython. Mais la demande de fonctionnalité numéro un était : « est-ce que ça prend en charge NumPy ? »

    • Fait intéressant, MicroPython suscite désormais aussi de l’intérêt et des demandes pour des usages web, grâce à l’excellent travail de PyScript. Mais contrairement aux microcontrôleurs qui ont 1 Mo de RAM au total, sur le web on s’attend à ce que le code existant tourne et à disposer de l’expérience CPython complète ainsi que de sa compatibilité, ce qui est beaucoup plus difficile.
      En même temps, il semble que côté CPython, des travaux soient aussi en cours pour mieux fonctionner sur le front-end, et le principal point de douleur est la taille des paquets.
  • J’ai appris quelque chose de similaire en créant une startup. Si c’était à refaire, j’aurais activement évité les fonctionnalités minimales pour entrer sur le marché de notre domaine.
    À la place, nous aurions dû construire seulement le strict minimum pour donner confiance dans le fait que notre architecture pouvait gérer les exigences de type entreprise, puis concentrer tous nos efforts sur les éléments différenciants capables de susciter une réaction du genre « ah, je vois jusqu’où ça peut aller », plutôt que sur des fonctionnalités qui font dire « c’est juste un clone de X ».

    • Je vois l’idée, mais le terme « table stakes » est mal employé. Par définition, ce sont les mises qu’il faut obligatoirement poser pour participer à la partie.
      La stratégie décrite ressemble plutôt à : implémenter les fonctionnalités indispensables de base, mais ne pas aller trop loin dans les extensions « courantes » qui viennent ensuite. Autrement dit, créer des fonctionnalités intéressantes qui donnent envie de vous recontacter, tout en ayant assez de bases pour ne pas être éliminé faute d’éléments essentiels.
    • Si j’ai bien compris, cela veut dire implémenter le minimum nécessaire pour les clients entreprise — ce que leur direction exige pour approuver la transaction —, oublier le reste, puis se concentrer sur quelque chose de vraiment nouveau qui vous distingue des concurrents ?
    • C’est un peu comme le premier iPhone, qui n’avait même pas le copier-coller et dont les performances radio GSM étaient très décevantes.
  • Je ressens la même chose pour tout code wrapper. Il arrive que quelqu’un dise : « il nous faut une version interne de cette API »
    Les raisons varient, mais cela revient souvent à « on ne peut pas faire confiance aux gens pour utiliser correctement l’API officielle ». C’est possible, mais la version interne sera moins standard et moins bien documentée
    Parfois, la raison est : « il nous faut des fonctionnalités supplémentaires » ; dans ce cas, il suffit d’ajouter trois fonctions, pas d’envelopper toute l’API. Avec le temps, 99 % de la base de code peut devenir un polyfill
    L’idée principale est que ne pas utiliser les valeurs par défaut finit par infliger une grosse douleur à la personne qui héritera de la base de code plus tard

    • Pour ce genre de wrapper, je pense qu’il est utile d’avoir une interface permettant d’interagir directement avec la bibliothèque sous-jacente
      Par exemple, la bibliothèque ziggy-pydust, qui permet d’écrire des modules natifs Python en Zig, est nettement plus agréable à lire qu’un import Python.h classique. Même si ce n’est pas encore implémenté, elle dispose aussi de .ffi pour accéder directement aux fonctions présentes dans Python.h
      Sans ce genre d’option, on finit généralement par abandonner ce type de bibliothèque et préférer l’originale. Cela dit, dans un certain sens, c’est aussi un wrapper, à savoir un module natif, et pour aller vite en développement, il est parfois préférable d’utiliser directement ctypes
  • C’est un bon article, avec beaucoup d’excellentes leçons, mais il manque un ingrédient essentiel. Cela ressemble aussi à n’importe quelle alternative concurrente à un produit
    C’est un peu comme dire qu’Amazon a échoué parce qu’il n’avait pas les librairies physiques auxquelles les gens étaient habitués, alors qu’en réalité ce n’est pas ce qui s’est passé
    Si ces alternatives JIT ont échoué et n’ont fait que courir après le retard, c’est parce qu’en pratique la plupart des développeurs du langage X n’accordent pas tant d’importance au JIT. Plus précisément, ils accordent plus d’importance aux fonctionnalités du langage et à l’interopérabilité qu’au JIT
    C’est donc le produit qui « rejoint » plutôt que celui qui concurrence qui l’emporte. Parce qu’il ne peut pas offrir une stabilité ou une interopérabilité supérieures

  • Je travaille depuis longtemps avec les langages et les compilateurs, et j’ai trouvé cet article très intéressant. Pour dire la même idée autrement, un langage représente bien plus que la simple vitesse de compilation
    La vitesse de compilation est très importante, et elle figure clairement parmi les dix dimensions principales. En particulier, l’accélérer rend la boucle de feedback des développeurs plus rapide, et permet à l’équipe cœur d’améliorer plus vite toutes les autres dimensions
    Mais il existe tout de même plus de 30 autres dimensions très importantes pour un langage de programmation

    • L’article semble porter sur la vitesse d’exécution. Mais même là, au vu de la popularité de CPython, la vitesse d’exécution n’est clairement pas le facteur numéro un
  • La conclusion sur la manière dont ce type de projet peut réussir est bonne, mais il y a un facteur encore trop peu mentionné pour expliquer pourquoi beaucoup de projets ne décollent pas : la compatibilité des implémentations alternatives est souvent, en réalité, inférieure à ce qui est annoncé, y compris pour des fonctionnalités anciennes du langage
    Par exemple, il est très fréquent que des applications Ruby et Python aient quelque part dans leurs dépendances une extension native en C, et à ma connaissance les principales implémentations alternatives n’ont jamais pris cela en charge. Il y a eu des tentatives, mais elles n’ont pas bien fonctionné pour des raisons techniques évidentes, et l’autre solution consistant à attendre des bibliothèques qu’elles fournissent plusieurs implémentations n’a pas eu une histoire très heureuse
    Si l’on ajoute à cela le fait que ces langages sont souvent utilisés pour des sites web CRUD où les entrées/sorties pèsent davantage sur les performances que le CPU, l’attrait d’une alternative plus rapide diminue fortement

  • Vraiment un excellent article. La sociologie de la technologie est fascinante
    Les mainteneurs d’implémentations de langage veulent un maximum de flexibilité lorsqu’ils conçoivent et déploient de nouvelles fonctionnalités utiles aux utilisateurs. Ils ne veulent pas être freinés par la nécessité d’obtenir l’accord de plusieurs implémentations avant de publier une fonctionnalité. Il suffit de regarder à quel point l’évolution de JavaScript a semblé glaciaire pendant des années, et à quelle vitesse TypeScript évolue en comparaison
    En même temps, les implémentations alternatives peuvent être le signe d’un écosystème de langage robuste, elles ont donc aussi des avantages. Si une implémentation alternative est vraiment bonne, même si elle ne l’est que pour certains utilisateurs aux besoins de niche, elle peut apporter une vraie valeur à l’écosystème
    Donc, si l’on est concepteur ou mainteneur d’un langage, on ne sera probablement pas activement hostile aux implémentations alternatives, mais elles ont des inconvénients. En général, les retours des utilisateurs iront dans le sens de publier de nouvelles fonctionnalités et de faire évoluer le langage. Il y aura peu de demandes pour ralentir afin de permettre à PyPy, IronRuby, LuaJIT, etc., de suivre
    Lorsqu’un utilisateur d’un langage choisit sur quelle implémentation construire, sa priorité principale est généralement la sûreté et la stabilité. Personne ne veut qu’une base de code d’un million de lignes dépende des subtilités de comportement d’une implémentation alternative créée autrefois par un doctorant très brillant, mais parti depuis sur un autre projet. Les utilisateurs se regroupent donc autour de l’implémentation la plus utilisée, et ce fait attire à son tour d’autres utilisateurs, créant une forte boucle de rétroaction positive
    Au final, en l’absence d’une force puissante poussant dans la direction opposée, la plupart des langages convergent vers une seule implémentation de référence. On peut soutenir que c’est une bonne chose, car presque tous les efforts d’ingénierie consacrés à l’implémentation du langage profitent à tous les utilisateurs au lieu d’être fragmentés entre plusieurs implémentations. L’inconvénient, bien sûr, est que l’implémentation peut rester bloquée dans un optimum local

  • LuaJIT a été mentionné, mais c’est aussi un exemple montrant que les choses ne se passent pas toujours comme le conclut l’article. Beaucoup de personnes et de projets ont délibérément choisi LuaJIT plutôt que Lua

    • Je suis venu dans les commentaires pour dire cela. LuaJIT est utilisé avec enthousiasme dans la communauté TeX, et l’exécutable luajittex est aussi facilement disponible aux côtés de luatex
  • C’est peut-être une opinion moins populaire, mais les gens devraient de temps en temps examiner leur ego, eux-mêmes compris. Il peut être plus facile de créer un projet parallèle qui soit « à soi » que de contribuer à un projet open source existant, mais il faut se demander pour qui on le fait
    Est-ce pour les mainteneurs du projet, pour le projet lui-même, pour les utilisateurs, ou pour son propre ego ? Si le dernier point vous met en colère, il y a probablement un lien
    Ajouter un JIT à un langage existant est un chantier énorme, et l’implémentation de référence aura donc forcément des critères d’acceptation élevés. Malgré tout, c’est selon moi vers cela qu’il faut viser. Forker ou repartir de zéro donne aussi la liberté d’accomplir de grandes choses, mais il faut souvent le voir comme quelque chose de provisoire
    Si l’objectif est de montrer ce dont je suis capable, il y a de fortes chances que cela n’aille pas très loin. Si l’objectif est de faire quelque chose de meilleur, on apprend à travailler dans les contraintes des autres

    • Étonnamment, je pense l’inverse. Ce sont précisément ceux qui prêchent qu’il ne faut contribuer qu’à un seul projet, au projet de référence, et qu’il ne devrait pas y avoir d’implémentations alternatives, qui devraient examiner leur ego et comprendre l’existence de plusieurs approches ainsi que pourquoi la concurrence vaut mieux que le monopole
      Je pense que cet article montre comment Python, Lua et Ruby ont adopté cette approche et déçu beaucoup de monde. Résultat : des milliers de développeurs et des millions d’utilisateurs doivent supporter un développement et des logiciels plus lents. Non pas parce que c’est impossible, mais parce qu’administrativement, il n’y a aucune incitation à faire autrement
    • Hmm, il arrive aussi que l’implémentation officielle soit complètement maudite, et qu’il faille alors créer son propre petit jardin. Même s’il finit par devenir un domaine de 20 acres avec un plan de rotation des cultures, c’est parfois préférable à un volcan