Design: Suite

Créé le 7 janv. 2019  ·  38Commentaires  ·  Source: WebAssembly/design

Un moyen de sauvegarder l'état d'exécution de WebAssembly et de le restaurer ultérieurement serait utile afin de porter les API bloquantes vers des plates-formes dont les API équivalentes sont asynchrones, en particulier le Web lui-même, qui repose fortement sur les API asynchrones. Seule une forme très limitée de suites serait nécessaire pour ouvrir un large éventail de possibilités.

La recherche de « continuations » dans le suivi des problèmes fait apparaître quelques commentaires de @rossberg mentionnant des plans pour des « continuations délimitées »/« commutation de pile » dans une révision ultérieure de WebAssembly : https://github.com/WebAssembly/design/issues/ 796#issuecomment -403487395, https://github.com/WebAssembly/design/issues/919#issuecomment -349038338, https://github.com/WebAssembly/design/issues/1171#issuecomment -355967209. Il ne semble pas y avoir de mention de la fonctionnalité sur le site Web, ni de problème dédié dans ce référentiel, j'aimerais donc demander : quel est le statut ici ?

Commentaire le plus utile

Pour faire suite à certains de mes commentaires ci-dessus, je devrais probablement créer un lien vers ma présentation de la réunion de février 2020, montrant des idées pour ajouter des gestionnaires de commutation/coroutines/continuations/effets à Wasm .

Tous les 38 commentaires

Je pense que ces fonctionnalités seront implémentées en tant qu'extension (généralisation) de la prise en charge de la gestion des exceptions, de sorte que l'achèvement est probablement l'objectif actuel.

Je suis d'accord que ce serait bien d'avoir plus d'une feuille de route pour ce qui vient après, oui.

Il y avait une description de cela ici , bien que la proposition de gestion des exceptions ait changé depuis lors, alors peut-être que ce n'est plus pertinent. Peut-être que @rossberg et @aheejin peuvent fournir plus d'informations.

Certaines personnes parmi nous travaillent actuellement sur une conception expérimentale qui généralise la proposition d'exception actuelle avec reprise, produisant l'équivalent de gestionnaires d'effets, qui sont une forme structurée et typable de continuations délimitées. Nous avons une sémantique de base mais il est beaucoup trop tôt pour une proposition. Les prochaines étapes seront le prototypage dans l'interpréteur de référence et l'expérimentation de la conception et peut-être de certains producteurs (en tant que produit secondaire, cela donnerait une implémentation de référence et un proxy de spécification pour la proposition d'exception).

Mais quelles que soient les spécificités de la conception, le mécanisme fondamental de création, de gestion et de basculement entre plusieurs piles d'exécution ne s'ajoute pas facilement aux machines virtuelles existantes, c'est le moins qu'on puisse dire. Ce sera le plus grand obstacle. Il y a un certain intérêt, par exemple pour l'équipe V8, mais je pense que sa mise en œuvre et l'évaluation de la viabilité des performances prendront un temps considérable.

Ce commentaire est un peu long, mais soyez indulgents avec moi, je vous promets que tout est utile :

Je travaille actuellement sur une implémentation alternative d'Erlang basée sur une compilation en amont plutôt que sur une interprétation de bytecode ; la cible principale est WebAssembly, mais d'autres cibles seront éventuellement prises en charge pour prendre en charge l'utilisation en tant que compilateur de code natif général pour les langages BEAM. Pour implémenter le langage et le compilateur, quelques exigences clés doivent être remplies :

  • Appels de queue appropriés, comme la récursivité est largement utilisée dans le langage, il n'y a pas d'autre moyen d'exprimer l'itération
  • Retours non locaux bon marché, le throw Erlang est couramment utilisé pour simplifier le code qui doit sortir tôt, ou pour permettre de renvoyer une valeur depuis le plus profond d'une pile d'appels ; ce n'est pas quelque chose qui est utilisé uniquement pour les exceptions
  • Planification pseudo-préemptive (implémentée en tant que planification coopérative dans le runtime, mais semble préemptive à partir du code Erlang), qui est utilisée pour implémenter des threads verts (appelés processus en Erlang)
  • ramasse-miettes par processus ; important pour la performance, en particulier pour éviter d'arrêter le monde à collecter. Dans de nombreux cas, les processus Erlang meurent avant qu'un GC de ce processus ne doive être effectué, ce qui signifie que de grandes étendues de mémoire n'ont jamais besoin d'être analysées.

Pour créer un compilateur à l'avance qui répond à ces exigences, j'ai choisi d'utiliser le style de passage de continuation comme décrit dans Cheney sur le MTA qui fournit une solution qui répond avec élégance à chaque exigence. En bref, voici une description de son fonctionnement et de la manière dont il résout mes problèmes en tant qu'implémenteur de langage :

  • Un programme est converti au format CPS. Dans la forme CPS, et dans cette version en particulier, chaque appel est en position de queue, et l'appelé reçoit à la fois un retour et une suite d'échappement (suites dites "à double barillet") en plus de tous les arguments.
  • Puisqu'aucun appel ne revient jamais, la pile finirait par exploser, mais l'astuce utilisée dans _Cheney sur le MTA_ est que le point d'entrée prend l'adresse du pointeur de pile et l'utilise pour vérifier périodiquement (généralement chaque appel de fonction) pour déterminer si le pile est sur le point de manquer d'espace. Lorsque cela se produit, les valeurs en direct sur la pile sont déplacées vers le tas, avec la continuation actuelle, et un longjmp est utilisé pour revenir au point d'entrée (qui agit comme un trampoline). Cela réinitialise efficacement la pile à la case départ, avec très peu de frais généraux.
  • Le saut nous donne également la possibilité d'effectuer le ramasse-miettes, la planification et, en conséquence de la réinitialisation de la pile, garantit que les appels de queue ne font jamais sauter la pile. Des points de rendement supplémentaires peuvent être ajoutés au-dessus et au-delà de l'état actuel du pointeur de pile, comme le comptage de réduction (comment Erlang détermine quand préempter un thread vert) ou déterminer si un important garbage collection est nécessaire.
  • L'utilisation de continuations signifie que nous obtenons facilement des rendements non locaux bon marché
  • L'utilisation de continuations signifie qu'il est facile de suspendre un fil vert et d'en reprendre un autre

Rien de ce que j'ai décrit ci-dessus ne nécessite en fait une prise en charge explicite de la continuation de la machine - cela ne nécessite que des moyens par lesquels la taille de la pile peut être vérifiée (pour laquelle nous avons déjà des solutions pour aujourd'hui, même si un peu bidon) et quelque chose comme setjmp / longjmp pour permettre de rembobiner la pile à moindre coût.

D'après les recherches que j'ai effectuées jusqu'à présent, l'utilisation de setjmp / longjmp dans WebAssembly via des exceptions JS entraîne une pénalité de performances significative et n'est fondamentalement qu'une cale de compatibilité. Cela ralentit inutilement tout langage utilisant une approche de compilation de style Cheney sur le MTA. Chicken Scheme est un exemple assez connu d'un tel langage, et je pense que Chez Scheme utilise également une astuce similaire, sinon la même.

Une alternative consiste à utiliser des trampolines - c'est-à-dire plutôt que longjmp lorsque la pile est sur le point d'exploser ou qu'un point de rendement est nécessaire, le compilateur génère du code qui retourne à un trampoline chaque fois qu'il faut invoquer la continuation suivante. Le problème bien sûr est qu'il encourt un retour + appel de fonction supplémentaire pour chaque continuation appelée ; Cependant, je soupçonne que c'est mieux aujourd'hui dans WebAssembly par rapport au hack setjmp / longjmp actuel. On pourrait également utiliser un return explicite pour dérouler la pile, mais je soupçonne que c'est encore plus lent que la solution setjmp / longjmp .

Tout cela pour dire, je pense que quelque chose est nécessaire pour rendre la mise en œuvre de langages avec des exigences similaires à la fois réalisable et performante. Peut-être que cela signifie une sorte de prise en charge native de la continuation, ou peut-être que cela signifie quelque chose de moins robuste, comme la prise en charge du modèle setjmp que j'ai mieux décrit. Je veux être clair qu'il ne s'agit pas seulement d'un problème spécifique à mon implémentation - l'ensemble de langages qui ont ou peuvent avoir ces exigences a des membres non triviaux, par exemple Haskell, toute implémentation de schéma conforme aux normes, Erlang (et BEAM langues en général), et essentiellement toute langue qui dépend d'une prise en charge appropriée des appels de queue, et pas seulement la version limitée rendue possible par la convention d'appel fastcc.

@rossberg Y a-t-il un endroit où je peux revoir l'esquisse de ce sur quoi vous travaillez avec la proposition d'exception + la reprise ? J'aimerais creuser et voir si cela contient une solution pour moi ou non. Si c'est le cas, et je peux vous être utile, je suis heureux d'essayer de donner un coup de main également pour continuer à avancer.

Si vous avez des questions (ou même des suggestions !) sur ce qui précède, faites-le moi savoir !

@bitwalker Ce qui n'est pas clair pour moi d'après les options que vous décrivez :

  • L'utilisation de longjmp impliquerait que vous utilisez la pile wasm réelle (qui n'est pas sous votre contrôle), ce qui signifie que vous n'avez aucun moyen de déterminer combien vous utilisez et quand vous êtes sur le point de manquer d'espace de pile , actuellement?
  • Si, à la place, vous utilisiez votre propre "pile d'ombres" en mémoire linéaire pour stocker les cadres de pile, vous pourriez la réinitialiser sans utiliser longjmp , en supposant que vous n'utilisez pas de fonctions wasm pour implémenter les fonctions dans votre langage, mais que vous ayez à la place tous les corps vivent à l'intérieur d'une énorme construction for(;;) switch(..). Cela peut être plus lent pour d'autres raisons (difficile à faire l'allocation des registres ?), mais au moins ne nécessite aucune aide de wasm.
  • L'utilisation d'un trampoline (avec la pile wasm) exclurait les utilisations où le cadre de la pile doit survivre, comme les fils verts ?

Soit dit en passant.. "Cheney sur le MTA" est en effet élégant.. c'est presque comme un collecteur de copie, mais contrairement à un GC complet, une liste de cadres de pile est triviale à copier. J'adore aussi que Baker serve ce papier sous la même forme à partir de la même URL depuis plus de 20 ans :)

@aardappel Bien sûr, permettez-moi de clarifier :)

L'utilisation de longjmp impliquerait que vous utilisez la pile wasm réelle (qui n'est pas sous votre contrôle), ce qui signifie que vous n'avez aucun moyen de déterminer combien vous utilisez et quand vous êtes sur le point de manquer d'espace de pile , actuellement?

Cela est vrai sur plus de plates-formes que WebAssembly, mais l'astuce consiste à définir d'abord une taille de pile maximale arbitraire (par exemple, la plus petite taille de toutes les plates-formes que vous prenez en charge si vous ne souhaitez pas compiler de manière conditionnelle avec des constantes différentes), et alors la toute première chose à faire à l'entrée est de prendre l'adresse de quelque chose sur la pile (donc pas _techniquement_ le pointeur de pile, mais peut essentiellement être utilisé comme un), ou d'obtenir le pointeur de pile lui-même. Compte tenu de votre taille maximale, du pointeur de pile (approximatif ou non) et du fait de savoir si la pile augmente ou diminue sur une cible donnée, vous pouvez vérifier la quantité de tampon dont vous disposez entre le maximum que vous avez défini et l'adresse de pile que vous détenez .

Si, à la place, vous utilisiez votre propre "pile d'ombres" en mémoire linéaire pour stocker les cadres de pile, vous pourriez la réinitialiser sans utiliser longjmp , en supposant que vous n'utilisez pas de fonctions wasm pour implémenter les fonctions dans votre langage, mais que vous ayez à la place tous les corps vivent à l'intérieur d'une énorme construction for(;;) switch(..). Cela peut être plus lent pour d'autres raisons (difficile à faire l'allocation des registres ?), mais au moins ne nécessite aucune aide de wasm.

Vous avez à peu près résolu un problème majeur ici - cela détruit la capacité d'optimiser l'allocation des registres, ce qui n'est peut-être pas un problème avec WebAssembly (?), compte tenu de la sémantique de sa machine à pile, mais c'est certainement quelque chose que j'éviterais à moins que je n'aie pas d'autre choix . Un problème plus important est qu'il empêche toute une série d'optimisations dans le backend du compilateur (j'utilise LLVM, mais j'ai entendu dire que le problème est le même avec diverses implémentations de langage de compilation vers C).

L'utilisation d'un trampoline (avec la pile wasm) exclurait les utilisations où le cadre de la pile doit survivre, comme les fils verts ?

Cela peut aider à comprendre comment l'exécution se déroule lors de la planification. Supposons donc que nous ayons un processus (lire: Erlang green thread) en cours d'exécution, et lors de l'entrée dans la fonction actuelle, et après avoir vérifié si la pile doit être réinitialisée et si un GC majeur est requis, vérifie son nombre de réductions (essentiellement une approximation de la quantité de travail qu'il a effectué depuis le dernier rendement) et voit qu'il doit céder au planificateur car il va dépasser son quota :

  1. Control se ramifie vers ce que j'appelle un "point de rendement", où une continuation est construite qui pointe vers la fonction actuelle et référence les continuations de retour et d'échappement actuelles, ainsi que tout autre argument.

    • Ces arguments représentent tous des valeurs vivantes sur la pile "réelle" - sous forme CPS, tout ce qui n'est pas un argument de la fonction actuelle est mort ou sur le tas.

  2. Toutes les valeurs en direct qui se trouvent sur la pile sont déplacées vers le tas (dans notre cas, chaque processus a son propre tas, donc les valeurs y sont déplacées).
  3. La continuation construite est stockée dans la structure du processus (chaque processus/fil vert a une structure avec toutes ses métadonnées clés, telles qu'un pointeur vers son tas, le nombre de réductions, etc.
  4. Un saut est effectué, qui rend le contrôle à l'ordonnanceur.
  5. Le planificateur extrait le prochain processus à planifier de sa file d'attente d'exécution, obtient la continuation de ce processus et l'appelle avec les arguments stockés dans cette continuation.

Donc, dans cet esprit, l'utilisation d'un trampoline n'empêche pas ce flux, mais impose simplement une surcharge coûteuse - plutôt que de pouvoir appeler une continuation directement (en fait juste un pointeur de fonction à ce stade), en utilisant des arguments déjà sur la pile ; une continuation doit être construite, et les valeurs vivantes déplacées vers le tas, avant de retourner au trampoline. Faire cela pour chaque appel est nettement plus lent que de construire uniquement une continuation lors de la planification, du GC ou d'une réinitialisation de la pile.

Soit dit en passant.. "Cheney sur le MTA" est en effet élégant.. c'est presque comme un collecteur de copie, mais contrairement à un GC complet, une liste de cadres de pile est triviale à copier. J'adore aussi que Baker serve ce papier sous la même forme à partir de la même URL depuis plus de 20 ans :)

J'ai pensé que l'utilisation de la pile en tant que jeune génération est un coup brillant. Et ça va mieux ! Puisque nous sommes sous forme CPS, vous n'avez pas besoin de copier les cadres de pile, les seules valeurs en direct qui doivent être copiées _doit_ être dans, ou référencées par, les arguments de la fonction actuelle, tout ce qui n'est pas accessible à partir de ces arguments est mort, et efficacement collecté par le saut :)

Avant de rencontrer le journal, je suis triste de dire que je ne connaissais pas son travail, mais Baker est génial, je suis un grand fan, d'autant plus que comme vous le dites il continue d'héberger tous ses papiers après toutes ces années , essentiellement inchangé !

Je tiens également à souligner que bien qu'il y ait un certain croisement ici avec la proposition d'appel de queue, j'ai l'impression que mon point est plus d'argumenter pour soutenir les continuations en tant que stratégie de mise en œuvre du langage, d'une manière ou d'une autre. Les appels de queue appropriés sont essentiellement le problème, bien sûr, mais la leçon clé derrière _Cheney sur le MTA_ est que le manque de support natif pour les appels de queue peut être contourné et rester relativement efficace, avec les bons outils.

Plus précisément, la proposition actuelle d'appel de queue est imparfaite à mon avis, car elle décrit essentiellement un trampoline (il faut encourir le coût d'un retour + appel). L'un des principaux avantages des appels de queue est qu'ils sont efficaces, essentiellement équivalents à un goto local. Sans cela, je ne suis pas sûr de l'avantage que procure une instruction d'appel de queue dans la spécification, au-delà d'un compilateur n'ayant pas besoin de générer ses propres trampolines. Il sera peut-être possible pour les machines WebAssembly d'optimiser ces instructions en "vrais" appels de queue, mais je n'ai vu aucune discussion à ce stade, et c'est une discussion critique.

Je ne sais pas quelle est la bonne solution, mais je pense qu'un indicateur clé pour savoir si WebAssembly a les bonnes primitives est de savoir si les programmes fonctionnels peuvent être compilés en un code qui est essentiellement aussi performant que ses homologues impératifs (à l'exception d'autres problèmes, tels que en tant que données immuables), ce qui revient à prendre en charge efficacement les appels de queue, d'une manière ou d'une autre.

la proposition actuelle d'appel de queue est imparfaite à mon avis, car elle décrit essentiellement un trampoline (il faut encourir le coût d'un retour + appel).

@bitwalker , j'ai peur de ne pas suivre. Pourquoi pensez-vous qu'il doit y avoir un trampoline? Ceci est certainement censé être un appel de queue régulier.

Reprise en main des exceptions / gestionnaires d'effets : il existe des exemples d'esquisses obsolètes ici . En dehors de cela, nous n'avons actuellement qu'une formulation d'une sémantique formelle, sans aucune explication textuelle utilisable par écrit. Nous devrions probablement écrire quelque chose par la suite, mais il n'y a pas eu beaucoup d'activité.

J'ai peur de ne pas suivre. Pourquoi pensez-vous qu'il doit y avoir un trampoline? Ceci est certainement censé être un appel de queue régulier.

Je m'excuse si j'ai mal interprété ! Je faisais référence à cette section , où la sémantique d'exécution se comporte comme un return suivi d'un call . Cela semble être une façon déroutante de les expliquer, en particulier à cause de la connexion trampoline. J'ai toujours vu les cris de queue comme quelque chose qui s'apparentait davantage à un saut et c'est aussi mon impression de la façon dont les autres les perçoivent. Alors que le point principal des appels de queue est d'éviter de déborder la pile en réutilisant des cadres de pile, j'ai cru comprendre qu'une autre propriété clé des appels de queue est qu'ils évitent une grande partie du brassage de la pile qui se produit avec return + call ou même juste un appel; plus dans le cas d'appels auto-récursifs, mais encore plus généralement dans le cas d'appels à des fonctions avec des arguments à la même position, où le compilateur peut éviter de déplacer des choses. Je pense que je m'attendais juste à voir cela dans la proposition. Je ne vous dis rien que vous ne sachiez déjà sur les appels de queue, j'essaie juste d'expliquer pourquoi j'ai interprété la proposition comme je l'ai fait :)

En dehors de cela, nous n'avons actuellement qu'une formulation d'une sémantique formelle, sans aucune explication textuelle utilisable par écrit. Nous devrions probablement écrire quelque chose par la suite, mais il n'y a pas eu beaucoup d'activité.

Merci pour le lien! Il me semble que cela pourrait probablement être utilisé pour exprimer le type de flux de contrôle pour lequel j'utilise setjmp / longjmp (comme décrit ci-dessus, par exemple _Cheney sur le MTA_), en supposant que " le contrôle "resuming" dans un appel externe rembobine la pile d'appels à ce point. Est-ce le cas?

Quoi qu'il en soit, y a-t-il quelque chose que je puisse faire pour aider? Le problème est-il d'obtenir une mise en œuvre de référence, y a-t-il encore des questions ouvertes qui nécessitent une recherche/une discussion, ou a-t-il simplement besoin de quelqu'un pour le faire passer par le processus ?

Eh bien, étant donné que l'appelant et l'appelé peuvent avoir des listes de paramètres complètement différentes, les appels de queue ne peuvent généralement pas éviter le brassage d'arguments, même si cela se produit dans les registres. Bien sûr, les moteurs sont censés minimiser cela dans les cas où ils correspondent, mais cela n'est pas observable sémantiquement. Rien d'inhabituel ici, c'est comme ça qu'ils fonctionnent habituellement.

@Danielhillerstrom a commencé à pirater la conception du gestionnaire d'effets expérimentaux dans l'interpréteur de référence dans un premier temps, mais je doute que cela puisse être beaucoup parallélisé. La prochaine étape serait des expériences préliminaires pour le cibler, et c'est là qu'il pourrait utiliser toute l'aide que nous pouvons obtenir, si les gens essayaient de jouer avec, aider à itérer sur la conception, peut-être en écrivant un backend de compilateur expérimental le ciblant. Mais la partie la plus difficile, de loin, si nous y arrivons, sera une implémentation efficace de plusieurs piles / continuations (indépendamment des détails de conception concrets) dans un moteur de production, ce qui va être super délicat, étant donné la complexité totale du JS/Wasm existant VM.

Eh bien, étant donné que l'appelant et l'appelé peuvent avoir des listes de paramètres complètement différentes, les appels de queue ne peuvent généralement pas éviter le brassage d'arguments, même si cela se produit dans les registres

Mon principal problème est avec cette ligne:

Par conséquent, ils déroulent la pile d'opérandes comme le fait return

Cela ressemble beaucoup à cela, il faut effectuer le même travail qu'un retour + un appel, même si l'implémentation n'est pas strictement un trampoline. La raison pour laquelle je pense que la distinction est importante est qu'une caractéristique clé des appels de queue est qu'ils n'entraînent pas plus de temps système qu'un appel normal, en fait moins, car aucune trame de pile n'est allouée. Mais si les appels de queue font en fait deux fois plus de travail qu'un appel normal, alors c'est un prix énorme à payer pour les langages où les appels de queue ne sont pas seulement une optimisation.

Je ne dis pas nécessairement que la proposition nécessite des mises en œuvre pour le faire de cette façon, mais la façon dont elle est formulée n'est pas idéale. Les appels de queue ne "déroulent" pas vraiment la pile, ils écrasent leur propre cadre de pile, ce qui, je suppose, a l'apparence de dérouler la pile, mais ce n'est pas la même chose. Je ne voulais pas faire dérailler la conversation ici, mais je pense que la spécification devrait être explicite sur ces différences, à moins que l'intention ne soit d'éviter que les appels de queue soient implémentés comme des appels de queue appropriés.

@Danielhillerstrom a commencé à pirater la conception du gestionnaire d'effets expérimentaux dans l'interpréteur de référence dans un premier temps

Génial, ce travail est-il actuellement public ou est-il toujours piraté ensemble ?

La prochaine étape serait des expériences préliminaires pour le cibler, et c'est là qu'il pourrait utiliser toute l'aide que nous pouvons obtenir, si les gens essayaient de jouer avec, aider à itérer sur la conception, peut-être en écrivant un backend de compilateur expérimental le ciblant.

Une fois qu'il est disponible pour jouer avec, même s'il faut du travail pour le configurer de mon côté, je serais ouvert à l'écriture d'un backend WebAssembly distinct pour mon compilateur, plutôt que d'utiliser LLVM, ou peut-être même en personnalisant LLVM avec de nouveaux intrinsèques + baisse associée. WebAssembly est une cible importante pour moi, donc tout ce que je peux faire pour vous aider.

Mais la partie la plus difficile, de loin, si nous y arrivons, sera une implémentation efficace de plusieurs piles / continuations (indépendamment des détails de conception concrets) dans un moteur de production, ce qui va être super délicat, étant donné la complexité totale du JS/Wasm existant VM.

Ouais, je peux imaginer. Cela dit, je pense que les principaux moteurs sont tous fortement motivés pour le faire fonctionner, si WebAssembly doit devenir aussi fondamental qu'il le promet. Les ingénieurs de ces moteurs ont-ils déjà commenté cela? Est-ce le genre de chose qui est faisable et juste une question de temps, ou fondamentalement incompatible avec la façon dont ces moteurs fonctionnent aujourd'hui ? J'ai peu de recul sur la façon dont ces moteurs sont actuellement mis en œuvre, mais avoir une idée des contraintes sous lesquelles ils fonctionnent serait un contexte utile à coup sûr.

Mon principal problème est avec cette ligne:

Par conséquent, ils déroulent la pile d'opérandes comme le fait return

Ah, ne confondez pas le jargon de la machine abstraite avec la mise en œuvre. Cela fait référence à la pile d' opérandes dans la fonction actuelle, qui n'est qu'une notion de machine abstraite de la spécification et non matérialisée physiquement dans de vrais moteurs. Cela dit essentiellement que les opérandes inutilisés sont supprimés lorsque vous effectuez un appel. Tout cela est géré au moment de la compilation.

Cela dit, il n'y a aucune raison de croire que les appels de queue sont plus rapides que les appels réguliers en général . C'est un malentendu. Leur but est de réduire l'espace, pas le temps ! Ils peuvent très bien être un peu plus chers dans certains cas.

(Les instructions d'appel de queue dans certains moteurs commerciaux qui resteront sans nom sont connues pour avoir été jusqu'à 100 fois plus lentes - je suis convaincu que nous ferons mieux. :) )

Les ingénieurs de ces moteurs ont-ils déjà commenté cela?

Bien sûr. Tout le monde voit le besoin d'une certaine forme de commutation de pile. Cela ne veut pas dire que tout le monde n'a pas peur...

Ah, ne confondez pas le jargon de la machine abstraite avec la mise en œuvre. Cela fait référence à la pile d'opérandes dans la fonction actuelle, qui n'est qu'une notion de machine abstraite de la spécification et non matérialisée physiquement dans de vrais moteurs.

Désolé d'avoir raté ça ! Cela clarifie en effet pourquoi il est formulé de cette façon. Merci pour votre patience :)

@bitwalker

mais l'astuce est que vous définissez d'abord une taille de pile maximale arbitraire (disons, la plus petite taille de toutes les plates-formes que vous prenez en charge si vous ne voulez pas compiler de manière conditionnelle avec des constantes différentes), puis la toute première chose à faire à l'entrée est de prendre l'adresse de quelque chose sur la pile

Sauf que vous ne pouvez pas prendre l'adresse de quoi que ce soit sur la pile wasm, par conception. Au moment où vous prenez l'adresse d'une variable locale (dans LLVM ou par exemple C++), le code wasm émis change la variable locale d'un wasm local à quelque chose sur une pile fantôme. Vous ne pouvez donc pas mesurer la pile wasm de cette façon. Et vous avez déjà dit que vous seriez mécontent d'avoir tout sur une pile d'ombres.

Vous pouvez toujours émuler cela .. vous pouvez pour chaque fonction wasm calculer statiquement la taille totale de tous ses locaux + args + temps max, et l'utiliser comme une approximation conservatrice de la taille de la pile utilisée, puis exécuter votre nettoyage de pile (en utilisant longjmp ou exceptions) une fois qu'il dépasse une certaine limite conservatrice, vous estimez qu'il y aura une quantité correcte de pile wasm à utiliser sur n'importe quel moteur. Mais wasm ne spécifie pas la pile min/max qu'une implémentation doit fournir, c'est donc très incertain. Vous pouvez même essayer de détecter ce qu'une implémentation fournit en manquant volontairement d'espace de pile pour une variété de fonctions récursives, compter le nombre d'appels, attraper le piège et estimer un espace de pile conservateur lié à cela ;)

Encore une fois, je pense que vous êtes mieux avec une pile d'ombres, dans l'ensemble :)

Franchement, un module wasm devrait être autorisé à au moins faire allusion à la quantité d'espace de pile qu'il souhaite que le moteur fournisse, car sans une telle chose, vous ne seriez même jamais en mesure d'utiliser des algorithmes récursifs (qui traitent les données utilisateur, par exemple un analyseur ) dans wasm, _si la pile peut être arbitrairement petite_. Ou les backends auraient toujours besoin de forcer les algorithmes récursifs à utiliser la pile d'ombres. Cela aurait-il un sens @rossberg @titzer @binji ?

Donc, dans cet esprit, l'utilisation d'un trampoline n'empêche pas ce flux, mais impose simplement une surcharge coûteuse - plutôt que de pouvoir appeler une continuation directement (en fait juste un pointeur de fonction à ce stade), en utilisant des arguments déjà sur la pile ; une continuation doit être construite, et les valeurs vivantes déplacées vers le tas, avant de retourner au trampoline. Faire cela pour chaque appel est nettement plus lent que de construire uniquement une continuation lors de la planification, du GC ou d'une réinitialisation de la pile.

Ah ok, j'imaginais que vous vouliez dire un trampoline pour chaque call/callcc, mais vous voulez dire qu'il n'est idéalement utilisé que pour le rendement et quand vous manquez d'espace de pile ?

Avant de rencontrer le journal, je suis triste de dire que je ne connaissais pas son travail, mais Baker est génial, je suis un grand fan, d'autant plus que comme vous le dites il continue d'héberger tous ses papiers après toutes ces années , essentiellement inchangé !

Oui, tant d'idées "créatives", toujours d'actualité !

@aardappel :

un module wasm devrait être autorisé à au moins faire allusion à la quantité d'espace de pile qu'il souhaite que le moteur fournisse

Ce serait bien de fournir un moyen de configurer la taille de la pile, mais je vois deux problèmes avec les suggestions concrètes :

  1. La pile est intermodule, donc un indice par module n'a pas vraiment de sens.
  2. Il n'y a pas de métrique évidente permettant de spécifier l'espace de la pile, puisque l'utilisation de la pile est le produit de toute une gamme de variables.

Je ne sais pas comment résoudre ce problème. Ce serait mieux si la VM était juste capable de faire croître la pile. Ensuite, l'espace de pile ne serait délimité que par l'espace de tas.

@rossberg
1) Bon point, c'est un problème. Il est toujours utile de pouvoir le spécifier pour la majorité des cas cependant, où le module "lourd" (qui repose sur la récursivité) qui a été exécuté en tant que premier/module principal spécifie une telle limite, et tout module auxiliaire/enfant ne le fait pas. t avoir des exigences particulières. Ou si le redimensionnement de la pile est facile, cela pourrait être le maximum de tous les modules impliqués... bien que je suppose qu'il y aura des moteurs qui ne pourront pas redimensionner s'ils utilisent directement la pile de processus de l'hôte, donc tout est discutable.

2) Oui, c'est extrêmement dépendant de l'implémentation, mais c'est déjà le cas si j'écris un programme natif. Si je sais que Linux va me donner 8 Mo, j'examine ensuite ma fonction récursive pour estimer jusqu'où je peux aller et si cela est raisonnable pour les données les plus défavorables que je pense traiter... avant de décider de les transformer en un non- algorithme récursif ou non. C'est déjà une science très inexacte. Je ne vois pas pourquoi ce serait très différent pour wasm. Pouvoir dire "je m'attends à ce qu'il y ait de l'ordre de 1 Mo de pile" me donnera de bien meilleures garanties que d'avoir à écrire mon code en supposant que certains moteurs vont me limiter à 16 Ko de pile (et donc devoir supprimer toute récursivité de mon code). Idem pour le cas d'utilisation de @bitwalker .

Sauf que vous ne pouvez pas prendre l'adresse de quoi que ce soit sur la pile wasm, par conception. Au moment où vous prenez l'adresse d'une variable locale (dans LLVM ou par exemple C++), le code wasm émis change la variable locale d'un wasm local à quelque chose sur une pile fantôme. Vous ne pouvez donc pas mesurer la pile wasm de cette façon. Et vous avez déjà dit que vous seriez mécontent d'avoir tout sur une pile d'ombres.

Eh bien, peut-être qu'un exemple est nécessaire pour illustrer ce que je veux dire. Ce qui suit est une application C triviale qui utilise la récursivité pour incrémenter un compteur jusqu'à une limite arbitraire. Il utilise un trampoline, mais uniquement lorsque la pile va exploser, et il détermine quand retourner au trampoline en vérifiant combien d'espace de pile il pense qu'il lui reste :

#include <emscripten.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <stddef.h>

#define STACK_BUFFER 16384

// Checks whether the stack is overflowing
static bool stack_check(void);

// The address of the top of the stack when we first saw it
// This approximates a pointer to the bottom of the stack, but
// may not necessarily _be_ the exact bottom. This is set by
// the entry point of the application
static void *stack_initial = NULL;
// Pointer to the end of the stack
static uintptr_t stack_max = (uintptr_t)NULL;
// The current stack pointer
static uintptr_t stack_ptr_val = (uintptr_t)NULL;
// The amount of stack remaining
static ptrdiff_t stack_left = 0;

// Maximum stack size of 248k
// This limit is arbitrary, but is what works for me under Node.js when compiled with emscripten
static size_t stack_limit = 1024 * 248 * 1;

static bool stack_check(void) {
  // Get the address of the top of the stack
  stack_ptr_val = (uintptr_t)__builtin_frame_address(0); // could also use alloca(0)

  // The check fails if we have no stack remaining
  // Subtraction is used because the stack grows downwards
  return (stack_ptr_val - stack_max - STACK_BUFFER) <= 0;
}

#define MAX_DEPTH 50000

static int depth;
static int trampoline;

int a(int val);
int b(int val);

int a(int val) {
    // Perform stack check and only recurse if we have stack
    if (stack_check()) {
        trampoline = 1;
        return val;
    } else {
        depth += 1;
        return b(val); 
    }
}

int b(int val) {
    int val2;
    if (depth < MAX_DEPTH) {
        // Keeping recursing, but again only if we have stack
        val2 = val + 1;
        if (stack_check()) {
            trampoline = 1;
            return val2;
        }
        return a(val2);
    } else {
        return val;
    }
}

int main() {
    stack_initial = __builtin_frame_address(0); // could also use alloca(0)
    stack_max = (uintptr_t)stack_initial - stack_limit;

    double ts1, ts2 = 0;
    ts1 = emscripten_get_now();

    depth = 0;
    trampoline = 0;

    int val = 0;

    while (true) {
        val = a(val);
        // This flag helps us distinguish between a return to reset the stack vs
        // a return because we're done. Not how you'd probably do this generally,
        // but this is just to demonstrate the basic mechanism
        if (trampoline == 1) {
            trampoline = 0;
            continue;
        }
        break;
    }
    ts2 = emscripten_get_now();

    printf("executed in %fms\n", ts2-ts1);

    return 0;
}

Pour voir par vous-même, exécutez avec emcc -s WASM=1 demo.c -o demo.js et exécutez avec node demo.js . Mon installation de nœud local semble avoir une taille de pile maximale de 984 Ko, mais comme vous pouvez le voir, j'ai dû définir la taille de pile maximale dans ma démo à 248 Ko, car toute valeur supérieure entraînera un débordement de pile, et malheureusement il n'apparaît pas pour être un moyen d'accéder à la limite précise de la pile, il faut deviner ou définir une constante.

Veuillez pardonner le code horrible, mais je l'ai esquissé à la hâte. L'idée de base devrait être claire, j'espère - si l'on choisit de rebondir sur un trampoline uniquement lorsque la pile est épuisée, cela _est_ possible dans WebAssembly aujourd'hui, bien qu'avec certaines contraintes. Jusqu'à présent, dans mes divers profils de scénarios similaires, il s'agit essentiellement d'un lien entre l'approche ci-dessus et un trampoline ordinaire, mais je soupçonne que des scénarios plus réalistes montreraient que moins on doit revenir au trampoline, mieux c'est. Avec une si petite pile avec laquelle travailler, l'avantage est malheureusement assez faible - je m'attendrais à travailler avec une pile d'au moins 2 Mo, sinon plus, 248k est tout simplement ridicule. Quant à setjmp / longjmp , il ne s'en approche même pas en raison de la façon dont cela est implémenté aujourd'hui.

Comme l'a dit @rossberg , je ne pense pas qu'une taille de pile par module ait du sens, elle devrait être configurable globalement, ce qui est essentiellement le cas aujourd'hui, sauf que vous configurez la taille de pile globale pour le moteur lui-même. En ce qui me concerne, cela peut aussi signifier qu'il n'est pas du tout configurable, et c'est bien - la taille réelle n'est pas le problème principal, bien que la situation actuelle ne soit pas idéale. Permettre à la pile de grossir arbitrairement en grossissant dans le tas semble également être une option viable, mais probablement pas idéale car ce n'est pas vraiment ce à quoi on pourrait s'attendre. Je préférerais simplement voir une taille de pile plus tolérante (comme je l'ai dit ci-dessus, au moins 2 Mo).

Du point de vue de la spécification, je ne pense pas que l'on puisse _requérir_ une taille de pile spécifique, mais je pense que quelques éléments pourraient être ajoutés ou clarifiés :

  • Une recommandation concernant la taille minimale de la pile. Certaines plates-formes très restrictives devront probablement descendre plus bas, mais s'il y a une recommandation pour quelque chose comme 2 Mo, alors les plates-formes qui n'ont pas de restriction peuvent bénéficier de plus d'espace de pile pour travailler, mais la spécification n'est pas _imposant_ un le minimum
  • Le plus important est un moyen d'obtenir la limite de pile actuelle telle qu'elle s'applique au code WebAssembly en cours d'exécution (par exemple, la valeur de getrlimit(RLIMIT_STACK, ..) serait une limite utile pour le code compilé sur WebAssembly). Cela fournit aux implémentations de langage les moyens de gérer des piles de tailles différentes sans avoir à ajouter un tas de constantes conditionnelles par moteur, ou pire, une seule constante extrêmement faible pour le plus petit dénominateur commun.

Pour mon propre projet, je vais faire plus de profilage, mais d'après ce que je vois, je vais probablement utiliser un simple trampoline pour cibler WebAssembly, jusqu'à ce que setjmp / longjmp ou quelque chose comme les exceptions récupérables devient viable ; les appels de queue résolvent mon cas d'utilisation ; ou les tailles de pile dans les moteurs principaux deviennent suffisamment grandes pour que _Cheney sur le MTA_ puisse être utilisé. Les outils à ma disposition ne sont tout simplement pas suffisants pour que cela fonctionne bien aujourd'hui.

Je devrais également clarifier, j'ai compilé l'exemple ci-dessus avec emcc , mais il est configuré pour utiliser ma propre installation LLVM (celle utilisée pour cette démo est LLVM master, donc 9, et construite au cours des derniers jours - aucun changement de ma part), il est donc compilé avec le backend LLVM, _not_ asm.js - juste au cas où cela devrait faire une différence.

@bitwalker
Êtes-vous sûr que le code ci-dessus fait ce que vous pensez qu'il fait? Afaik, __builtin_frame_address (et alloca ) renverra des adresses sur la pile fantôme, alors que votre récursivité se produit sur la pile wasm, il devrait donc s'agir de valeurs sans rapport. Étant donné que vous ne stockez rien sur la pile d'ombres dans ce code, il se peut même qu'il ne grandisse pas, donc __builtin_frame_address peut retourner... la même valeur à chaque fois ?

@aardappel J'ai fait pas printf . J'ai également exécuté l'expérience avec la vérification de la pile activée et sans. Sans, le code n'atteint nulle part même près de 50k (la valeur MAX_DEPTH) et s'interrompt, ce qui est bien sûr attendu. Lorsqu'il est activé, le programme atteint 50k en ne frappant le trampoline qu'environ 6 fois, ce qui réussit. Si vous modifiez la quantité maximale d'espace de pile sur une quantité supérieure à 248k, disons 256k, la pile déborde, ce qui indique que le code revient correctement au trampoline lorsque nous atteignons la limite de pile définie. La valeur produite par __builtin_frame_address (qui est abaissée au @llvm.frameaddress intrinsèque) diminue du montant correct à chaque fois qu'un contrôle de pile est effectué (basé sur une estimation approximative en regardant les allocations dans le LLVM IR), il semble donc produire la valeur à laquelle je m'attendais.

J'ai essayé une implémentation avec getrlimit(2) , mais la valeur renvoyée par celle-ci est clairement déconnectée de la pile (la pile fantôme que vous mentionnez) et peut faire référence à la vraie pile - je ne suis pas sûr, mais ce n'est pas le cas pas utile pour la vérification de la pile. Il y a donc des outils qui ne peuvent pas être utilisés.

@bitwalker Je pense que cela peut être dû au fait que vous compilez sans optimisations. J'ai pris votre code et je l'ai compilé avec webassembly.studio, qui par défaut compile avec -O3 . Cela optimise complètement la boucle et tous les appels de fonction. Si j'ajoute un appel fictif, cela transforme le tout en boucle. https://webassembly.studio/?f=0aj0cn61ob8

@binji Oui, je compile sans optimisations, et c'est intentionnel en raison de la taille et de la simplicité de l'exemple - il se trouve que c'est quelque chose qu'un compilateur peut trivialement convertir en boucle dans ce cas. Des programmes beaucoup plus volumineux ne seraient pas optimisés de cette façon car cela nécessiterait essentiellement de définir l'ensemble du programme au sein d'une seule fonction.

Le but de l'exemple n'est pas de savoir si un compilateur peut optimiser ce cas dans une forme non récursive, mais que fondamentalement un _peut_ vérifier l'espace de pile disponible et agir dessus pour implémenter _Cheney sur le MTA_.

@bitwalker , comme l' a dit __builtin_frame_address est pour la pile d'ombres. Même si la valeur change, cela ne dit rien sur la place restante sur la pile d'appels système réelle, qui est celle qui vous intéresse. Il n'y a aucun moyen de mesurer ou d'observer cela depuis Wasm.

Votre code exploite un comportement non défini en C. Il se trouve qu'il fonctionne sur du matériel courant, mais pas sur Wasm.

Votre code exploite un comportement non défini en C. Il se trouve qu'il fonctionne sur du matériel courant, mais pas sur Wasm.

Pour être clair, j'exécute ce code dans un moteur WebAssembly, en particulier v8, sans compiler en x86_64.

Je sais que je regarde la pile fantôme dans l'exemple, mais c'est parce que je suppose que les allocations que je fais sur la pile (dans le code WebAssembly) sont allouées sur la pile fantôme, y compris les cadres de pile pour les fonctions appelées dans ce code . En d'autres termes, la pile système n'est même pas quelque chose dont je dois être conscient lors de l'écriture de WebAssembly ; la pile fantôme est la "vraie" pile dans ce contexte.

Si ce n'est pas le cas, alors oui, vous avez raison de dire que l'exemple fonctionne par hasard - mais cela semblerait impliquer qu'il n'y a aucun moyen pour un compilateur d'empêcher de générer du code qui pourrait déborder la pile système, ce qui semble comme un énorme problème pour moi. Est-ce vraiment le cas?

Les fonctions Wasm utilisent la pile système comme la plupart des autres codes. Le moteur implémente des gardes de pile, mais à part cela, il utilise la pile système comme n'importe quel autre compilateur et runtime. Et oui, ça peut déborder, mais de tels débordements se rattrapent. La pile fantôme est quelque chose introduite par le compilateur C, et elle ne contient que des locaux qui doivent être adressables en mémoire à partir de C. De toute évidence, le moteur ne pourrait pas y stocker des adresses de retour ou d'autres pointeurs internes sans créer de problèmes de sécurité majeurs.

Et oui, ça peut déborder, mais de tels débordements se rattrapent.

Le problème auquel je veux en venir est que cette distinction n'est pas utile si une implémentation de compilateur ciblant Wasm est incapable d'implémenter des contrôles d'exécution pour empêcher le débordement de la pile ou pour récupérer d'un débordement imminent, comme cela est possible sur d'autres cibles. Je ne comprends pas pourquoi il n'est _pas_ souhaitable de rendre disponible, au moins en lecture seule, le pointeur de pile système actuel, ainsi que la taille de pile maximale, pour ce faire. Il est clair que tout mettre sur une pile fantôme ne suffit pas à garantir quoi que ce soit, comme suggéré précédemment, donc les options pour les langages fonctionnels ici semblent vraiment affreuses, franchement.

Mon exemple C semble avoir brouillé les pistes plutôt que d'aider à illustrer, alors je m'excuse si cela a été plus difficile que cela n'en valait la peine. Je n'utilise ni C, ni ne le génère, je l'ai choisi pour garder l'exemple simple. Cependant, mon compilateur utilise LLVM et utiliserait les intrinsèques qu'il fournit pour Wasm, qui reposent naturellement sur les fonctionnalités définies dans la spécification. Mais il semble qu'il n'y ait aucun moyen pris en charge pour une implémentation de langage basée sur _Cheney sur le MTA_, comme le mien, de cibler Wasm sans refactorisation significative, ce qui, dans la plupart des cas, je suppose que c'est simplement un non-démarreur.

La question la plus importante pour moi est donc : quelle est la voie à suivre pour ces langages/implémentations ? De même, que peut faire la spécification pour les prendre en charge ? Je pensais que la plus grande pièce manquante était le manque de prise en charge native des appels de queue, mais il semble que les options typiques de contournement soient essentiellement indisponibles ou peu pratiques en raison de la surcharge. La seule exception est l'utilisation d'un simple trampoline - et si vous ne partez pas de zéro, reconcevoir votre implémentation autour de cela n'est au mieux pas trivial.

Le pointeur de pile système est une adresse matérielle physique. De toute évidence, il est hors de question d'exposer cela à du code non fiable sur une plate-forme sensible à la sécurité comme le Web, même si elle était en lecture seule et inaccessible.

En principe, Wasm pourrait fournir des moyens plus abstraits pour interroger et manipuler les tailles de pile. Mais les détails sur la façon de fournir un tel mécanisme ne sont pas évidents, voir ci-dessus. Surtout parce qu'elles introduiraient un degré substantiel de non-déterminisme observable. Une voie plus souhaitable serait d'avoir des mécanismes pour faire croître les piles dans les moteurs eux-mêmes plutôt que de faire pirater chaque programme autour de lui.

Une fois que la proposition d'appel de queue devient disponible, le problème devrait être raisonnablement mineur dans la pratique, cependant. Ce qui reste, ce sont des contraintes de ressources assez strictes et le fait que les programmes peuvent mourir de manière inattendue lorsqu'ils les font exploser, mais cela a toujours été la réalité du Web. Je pense qu'il est exagéré de dire que c'est prohibitif, et Wasm n'aggrave certainement pas les choses.

Le pointeur de pile système est une adresse matérielle physique. De toute évidence, il est hors de question d'exposer cela à du code non fiable sur une plate-forme sensible à la sécurité comme le Web, même si elle était en lecture seule et inaccessible.

Je souhaite en savoir plus sur la ou les vulnérabilités spécifiques que cela représente, mais je ne prétends pas que de véritables adresses matérielles sont nécessaires. À moins que quelque chose ne me manque, l'adresse du pointeur de pile accessible au code Wasm n'a besoin que d'un proxy utile pour la position relative dans la pile système dans laquelle le code s'exécute (par exemple, semblant être une adresse comprise entre 0 et n ), et la limite peut être arbitrairement inférieure à la taille réelle de la pile système. Cela vous permet d'interroger la pile sans exposer les adresses réelles en dehors du moteur lui-même.

Surtout parce qu'elles introduiraient un degré substantiel de non-déterminisme observable.

Pouvez-vous préciser ce que vous entendez par là ? Je parle spécifiquement de pouvoir interroger la taille de la pile, je ne pense pas que la manipulation soit un objectif. Il est certainement acceptable que différents moteurs aient des tailles de pile différentes, donc exposer cette taille n'introduirait pas plus de non-déterminisme que ce qui est déjà fondamentalement présent.

Une voie plus souhaitable serait d'avoir des mécanismes pour faire croître les piles dans les moteurs eux-mêmes plutôt que de faire pirater chaque programme autour de lui.

Cela traite d'un problème (la pile étant trop petite et donc trop facile à déborder), mais est tangent au problème que j'essaie de soulever.

Une fois que la proposition d'appel de queue devient disponible, le problème devrait être raisonnablement mineur dans la pratique, cependant.

Je suis d'accord, pour certains sous-ensembles de langues en tout cas, mais cette proposition semble encore être au début de son cycle, et le sera dans le temps à venir. En attendant, leur absence et le manque de solutions de contournement sont un énorme problème pour les langues qui en ont besoin. La proposition d'appel de queue elle-même ne contient pas de solution pour les langages qui utilisent la pile pour les allocations et le ramasse-miettes en sautant, mais l'idée d'exceptions avec reprise fournirait probablement une solution pour cela - mais cela n'est pas activement travaillé dans la mesure où Je suis conscient, c'est juste théoriquement possible une fois que la proposition d'exceptions est acceptée.

Ce qui reste, ce sont des contraintes de ressources assez strictes et le fait que les programmes peuvent mourir de manière inattendue lorsqu'ils les font exploser, mais cela a toujours été la réalité du Web. Je pense qu'il est exagéré de dire que c'est prohibitif, et Wasm n'aggrave certainement pas les choses.

Les contraintes elles-mêmes ne sont pas prohibitives, et ne sont pas vraiment le problème. Les contraintes de ressources et les risques associés existent également en dehors du Web, mais il existe des outils pour les gérer - dans le contexte des limites de la pile, on a setjmp / longjmp , en vérifiant la pile et les ordures ramassage par saut/retour lorsqu'il est sur le point de déborder, ou à l'aide de trampolines. Essentiellement, on peut s'assurer que si vous êtes sur le point de dépasser une limite, cela peut être géré avec élégance (ou simplement permettre que la limite soit dépassée et abandonner/paniquer). Le problème avec Wasm auquel j'essaie de trouver une solution est que ces outils (à l'exception des trampolines) sont supprimés (soit entièrement, soit en pratique comme c'est le cas avec SJLJ) et n'ont aucun remplacement. Pour les langages qui fondent toute leur implémentation autour de ces outils, leur absence est certainement un problème majeur. Je ne dis pas du tout que Wasm est fondamentalement défectueux ou quelque chose du genre, mais je pense qu'il manque quelque chose dans la spécification ici, en particulier, certains moyens de comprendre les contraintes actuelles sous lesquelles le code s'exécute (par exemple, la taille de la pile) et où vous êtes par rapport à ces contraintes (par exemple quelque chose qui ressemble à une adresse qui vous donne une idée de votre position par rapport à la taille maximale de la pile).

Si j'ai fondamentalement mal compris ou simplement raté quelque chose, je cherche vraiment à apprendre et à corriger cela. Il semble juste que c'est quelque chose qui a été négligé, basé sur la lecture des spécifications et l'examen de divers problèmes/propositions dans l'organisation. Si vous avez des liens vers d'autres discussions qui sont pertinentes ici, dirigez-moi certainement vers elles afin que je puisse éviter de ressasser tout ce qui a déjà été discuté, ou au moins m'assurer que j'apporte quelque chose de nouveau à la table.

@bitwalker :

Surtout parce qu'elles introduiraient un degré substantiel de non-déterminisme observable.

Pouvez-vous préciser ce que vous entendez par là ? Je parle spécifiquement de pouvoir interroger la taille de la pile, je ne pense pas que la manipulation soit un objectif. Il est certainement acceptable que différents moteurs aient des tailles de pile différentes, donc exposer cette taille n'introduirait pas plus de non-déterminisme que ce qui est déjà fondamentalement présent.

Actuellement, le code Wasm ne peut pas observer les débordements de pile, car il sera abandonné. Son propre résultat ne peut pas dépendre de la taille de la pile, seulement du succès contre l'échec. C'est assez différent de pouvoir réussir avec le résultat A ou avec le résultat B selon la pile.

Une fois que la proposition d'appel de queue devient disponible, le problème devrait être raisonnablement mineur dans la pratique, cependant.

Je suis d'accord, pour certains sous-ensembles de langues en tout cas, mais cette proposition semble encore être au début de son cycle, et le sera dans le temps à venir.

La proposition est à l'étape 3, elle devrait donc être disponible assez rapidement. Démarrer une proposition complètement différente à partir de zéro semble très peu susceptible d'aller plus vite.

Le problème avec Wasm auquel j'essaie de trouver une solution est que ces outils (à l'exception des trampolines) sont supprimés (soit entièrement, soit en pratique comme c'est le cas avec SJLJ) et n'ont aucun remplacement.

Mon argument était que ces outils n'ont jamais été disponibles sur le Web, il ne devrait donc peut-être pas être surprenant qu'ils ne soient pas disponibles dans Wasm non plus.

Mon argument était que ces outils n'ont jamais été disponibles sur le Web, il ne devrait donc peut-être pas être surprenant qu'ils ne soient pas disponibles dans Wasm non plus.

Bien sûr, mais maintenant que Wasm ouvre la possibilité de ces outils, la spécification ne devrait-elle pas s'efforcer d'identifier et de traiter ?

La question la plus importante pour moi est donc : quelle est la voie à suivre pour ces langages/implémentations ?

Pour l'instant, utilisez une pile d'ombres, un peu comme ce que fait votre code C dans une version non optimisée, et comme je l'ai mentionné dans ma première réponse.

Cela n'a pas besoin d'être désastreusement lent, vous pouvez toujours charger des variables dans les locaux à l'intérieur en tant que fonction unique et obtenir certains avantages de l'utilisation des registres. De plus, vous avez maintenant une manipulation de pile super bon marché puisque vous contrôlez tout cela.

Comme le dit @rossberg , bien que théoriquement possible, je ne pense pas qu'il soit probable que wasm fournirait un accès suffisant à la pile wasm pour que vous puissiez y implémenter toutes ces fonctionnalités.

Le nombre de langages qui s'écartent dans leur modèle d'exécution du modèle de type C est infini, nous ne les prendrons probablement pas tous directement en charge. En fait, nous ne prenons même pas totalement en charge le C directement :) Les appels de queue et les exceptions avec reprise seront utiles pour certains, mais "rouler vos propres cadres de pile" restera une option populaire, je prédis.

La proposition est à l'étape 3, elle devrait donc être disponible assez rapidement. Démarrer une proposition complètement différente à partir de zéro semble très peu susceptible d'aller plus vite.

Ce sont de bonnes nouvelles! Je n'ai pas vu beaucoup de mouvement cependant, et je ne sais toujours pas si la proposition réelle a des appels de queue appropriés ou quelque chose de plus restreint. Mais les appels de queue ne sont qu'une partie du problème ; la gestion de la mémoire est l'autre aspect clé de cela.

Allouer sur la pile lorsque cela est possible est toujours préférable, car sur la plupart des systèmes, la pile est une zone de mémoire dont l'accès est optimisé par rapport à la mémoire principale. Dans les cas où ce n'est pas le cas, il fonctionne comme n'importe quelle autre mémoire. Il s'ensuit donc que si la pile est principalement libre, car toutes les fonctions de votre programme sont des appels de queue et ne consomment donc pas d'espace de pile, alors vous voudriez utiliser cet espace de pile libre pour des allocations temporaires. Pour ce faire, vous devez savoir si une allocation réussira ou échouera, en vérifiant la quantité de pile qu'il vous reste. De plus, un tel modèle simplifie considérablement le ramasse-miettes, car on peut facilement ramasser l'espace de la pile en retournant à un trampoline ou en manipulant le pointeur de la pile. L'approche que vous utilisez n'a aucune importance ici, ce qui compte, c'est que vous puissiez dire _quand_ un GC doit se produire.

En tant qu'implémenteur de langage, vous ne voulez pas non plus avoir deux manières complètement différentes de gérer la mémoire selon que vous ciblez ou non WebAssembly. Ne pas avoir la pile optimisée n'est pas une grosse perte, mais devoir _éviter_ la pile sur une plate-forme signifie que vous devez modifier à la fois votre code d'allocation et les implémentations de la récupération de place de manière significative. C'est une charge énorme pour les compilateurs pour lesquels WebAssembly n'est qu'une cible possible. Dans les situations où cela est inévitable, il vous suffit de manger ce coût ou d'éviter le support de la plate-forme ; mais cela n'est vrai de WebAssembly que si aucune proposition pour effectuer ce changement n'aboutit.

Mon argument était que ces outils n'ont jamais été disponibles sur le Web, il ne devrait donc peut-être pas être surprenant qu'ils ne soient pas disponibles dans Wasm non plus.

Je ne suis pas surpris qu'ils ne soient pas disponibles _en ce moment_, mais je suis surpris et découragé d'entendre ce genre de réponse, en particulier de votre part, qui semble dire que parce que quelque chose n'a pas été disponible sur le Web historiquement, alors il ne vaut pas considération. WebAssembly est déjà utilisé en dehors du Web, et de tels cas d'utilisation sont même mentionnés dans les spécifications/matériaux de WebAssembly lui-même ! Sans oublier que WebAssembly apporte déjà des outils qui n'étaient pas disponibles auparavant ; les appels de queue n'étant que l'un d'entre eux.

Rien sur le Web (à ma connaissance) ne limite fondamentalement la possibilité d'interroger des informations sur l'espace de la pile - ce n'est qu'un détail d'implémentation du fonctionnement des moteurs aujourd'hui, cela n'a jamais été nécessaire dans JS, alors pourquoi existerait-il ? Si les développeurs de moteurs veulent le mettre à disposition de WebAssembly, ils le peuvent, mais je doute qu'ils soient enclins à le faire sans motivation. Je plaide dans le but d'essayer d'expliquer pourquoi c'est nécessaire. À cette fin, je travaille également à entrer en contact avec une variété d'ingénieurs de compilateur de langage fonctionnel pour recueillir plus de cas d'utilisation et de commentaires, que j'espère avoir bientôt partagés ici.

L'une des choses les plus inspirantes à propos de WebAssembly est qu'il a le potentiel de permettre une grande partie de ce qui n'était auparavant pas possible sur le Web, ou était possible mais à grands frais et à grands frais. Je suis généralement d'accord avec le sentiment que WebAssembly concerne avant tout le Web, mais à moins qu'il n'y ait un problème fondamental avec une proposition, je ne suis pas sûr que nous devrions fermer la porte aux idées simplement parce qu'elles ne sont pas déjà présentes dans une certaine forme. Après tout, n'est-ce pas à cela que sert le processus de proposition ici ?

Pour l'instant, utilisez une pile d'ombres, un peu comme ce que fait votre code C dans une version non optimisée, et comme je l'ai mentionné dans ma première réponse.

Cette option n'est pas viable, comme cela a été souligné précédemment, car il n'y a pas de correspondance directe entre la pile fantôme et la pile système ; cela signifie que la prévention des débordements, ce qui est l'essentiel, n'est pas garantie. La garantie est nécessaire pour les appels de queue, ainsi que pour décider quand effectuer des allocations sur la pile par rapport au tas et/ou déclencher le GC.

Cela n'a pas besoin d'être désastreusement lent, vous pouvez toujours charger des variables dans les locaux à l'intérieur en tant que fonction unique et obtenir certains avantages de l'utilisation des registres. De plus, vous avez maintenant une manipulation de pile super bon marché puisque vous contrôlez tout cela.

Comment suggérez-vous que j'implémente ma propre pile ? Compiler en code natif signifie utiliser la pile native, dans ce cas, la pile d'ombres Wasm, sur laquelle nous _n'avons pas_ le contrôle total. Compiler un programme dans un envoi de boucle/commutateur géant est une solution terrible, pour un certain nombre de raisons. La seule alternative actuelle est de trampoline pour chaque appel, mais comme mentionné précédemment, au-delà du coût de performance, il laisse de côté tout langage qui souhaite effectuer GC en utilisant un _Cheney sur le collecteur générationnel de style MTA_, ou utiliser la pile pour les allocations.

Le nombre de langages qui s'écartent dans leur modèle d'exécution du modèle de type C est infini, nous ne les prendrons probablement pas tous directement en charge. En fait, nous ne supportons même pas complètement C directement :)

D'après ce que j'ai vu, pratiquement tous les langages fonctionnels de conséquence (Haskell, Lisp, ML, Prolog) ont au moins un compilateur/implémentation qui génère du C, et les modèles que ces langages représentent couvrent essentiellement tout l'espace des langages de programmation fonctionnels. Erlang, par exemple, est assez similaire à ML dans sa représentation de base. Ces langages diffèrent tous significativement du C sémantiquement, mais néanmoins ils peuvent tous être traduits en quelque chose qui peut s'adapter au modèle de C. Je ne pense pas que nous parlions d'un problème de prise en charge de tous les détails d'implémentation possibles, mais avoir des primitives pour interroger l'utilisation de la pile est une préoccupation transversale qui affecte toutes sortes de langages, C inclus.

Les appels de queue et les exceptions avec reprise seront utiles pour certains, mais "rouler vos propres cadres de pile" restera une option populaire, je prédis.

Avez-vous un exemple de compilateur de langage qui cible Wasm, lance ses propres cadres de pile et n'est _pas_ un langage interprété ? Vous dites que c'est populaire, mais je ne le vois pas, mais peut-être que je cherche juste aux mauvais endroits. Je soupçonne que c'est aussi un problème pour FFI, mais je ne connais pas assez la mise en œuvre proposée pour le dire.

Peut-être devrais-je ouvrir un nouveau problème spécifiquement pour les primitives de pile que je pense nécessaires? Ce fil n'est plus _vraiment_ spécifiquement sur les continuations, et il ne s'agit pas non plus vraiment d'appels de queue. Il peut être utile d'appuyer sur le bouton de réinitialisation, pour ainsi dire.

@bitwalker

Peut-être devrais-je ouvrir un nouveau problème spécifiquement pour les primitives de pile que je pense nécessaires?

À cette fin, je travaille également à entrer en contact avec une variété d'ingénieurs de compilateur de langage fonctionnel pour recueillir plus de cas d'utilisation et de commentaires, que j'espère avoir bientôt partagés ici.

À ce stade, oui, je pense que vous devriez ouvrir une question distincte.

Je pense que vous devriez également faire une liste des primitives de pile exactes que vous demandez. Je sais qu'en dehors des fonctionnalités telles que les appels de queue et le SJLJ intégré qui sont déjà planifiés, vous aimeriez pouvoir prédire quand la pile d'appels interne peut déborder à l'avance. C'est tout ce dont vous avez besoin ?

À ce stade, oui, je pense que vous devriez ouvrir une question distincte.

Ça ira

...vous aimeriez pouvoir prédire quand la pile d'appels interne peut déborder à l'avance. C'est tout ce dont vous avez besoin ?

D'accord, je pense plus ou moins à refléter les primitives que l'on utiliserait ailleurs :

  • Limite de taille de pile, cela pourrait être arbitrairement plus petit que la limite de taille de pile _réelle_, mais permettrait d'atteindre l'objectif de fournir une limite supérieure sur la quantité de pile disponible.
  • Position de la pile, ce n'est pas un pointeur, mais plutôt une position relative à la limite de la pile. Cela permet d'évaluer la quantité de pile restante.

Mais ceux-ci pourraient être regroupés en un seul "espace de pile restant" primitif, qui ne vous fournit pas de limite ou de position, si cela est préférable. Étant donné que certaines choses vivent sur la pile fantôme Wasm et d'autres sur la pile hôte, il faut tenir compte des deux plutôt que d'une seule lors de la prise de décisions, et en ce qui concerne la pile hôte, je pense qu'il est seulement vraiment nécessaire de savoir combien d'espace reste disponible.

En les utilisant, un compilateur pourrait générer du code qui vérifierait la pile restante et déterminerait si un ramasse-miettes/trampoline est requis ou non, puis effectuerait les allocations de pile dont il a besoin, effectuerait n'importe quel calcul local, puis continuerait en appelant une continuation (généralement un autre call, mais peut être un saut local dans certains cas), qui dans le cas d'un appel, entrerait dans la nouvelle fonction et vérifierait à nouveau la pile restante et déciderait comment procéder. Une partie importante de ce flux est la capacité de savoir où les choses seront allouées, de sorte que la limite correcte soit vérifiée. Je ne suis pas sûr à 100% de ce que tout peut finir sur la pile hôte, je vais donc essayer de suivre cela aujourd'hui avant d'ouvrir un nouveau numéro.

Mon argument était que ces outils n'ont jamais été disponibles sur le Web, il ne devrait donc peut-être pas être surprenant qu'ils ne soient pas disponibles dans Wasm non plus.

Considérant qu'un langage comme le langage pyret de @shriram s'appuie sur un aperçu approfondi des échecs et des points de réussite des meilleurs langages (y compris javascript), son commentaire a du poids https://github.com/WebAssembly/design/issues/ 919#issuecomment -348000242. Javascript est une barre basse, le point principal pour cela est qu'il est omniprésent en raison de la participation à la diffusion de contenu sur des systèmes de réseau point à point. Les monopoles de diffusion de données apparaissent purement comme un comportement émergent de diffusion de données sur un système de communication point à point. Un langage conçu en 10 jours a chevauché la queue d'un lourd nœud tcp/ip destiné à diffuser des publicités, il n'est pas spécial et n'a pas atteint son statut actuel grâce à ses mérites. Nous avons l'opportunité de réparer les torts et de ne pas avoir besoin d'autant de frameworks pour réimplémenter à moitié les continuations. Le TCO est un grand pas dans la bonne direction.

Les continuations permettent des langages de programmation fonctionnels simultanés appropriés qui ont des réacteurs qui diffusent des données à plusieurs points (pas seulement des serveurs GAFAM !). Surtout lorsque les données deviennent directement adressables et que nous n'avons pas besoin d'obtenir des données indexées en adressant les nœuds de google. Il est avantageux pour Google d'avoir un environnement d'exécution à thread unique, car il fonctionne bien en détournant les publicités vers un canal client/serveur IP. Des continuations appropriées nous donnent de meilleures options. Le navigateur peut être bien plus que la machine de diffusion d'annonces délibérément paralysée qu'il est aujourd'hui.

Un peu tard pour cette conversation, dont je ne savais pas qu'elle se déroulait, mais @bitwalker peut trouver utile de jeter un œil (peut-être plus !) à Stopify [site : https://www.stopify.org ], [papier : https://cs.brown.edu/~sk/Publications/Papers/Published/bnpkg-stopify/ ], [dépôt : https://github.com/plasma-umass/Stopify ].

Il présente un compilateur JS->JS générique qui implémente des transformations similaires et inclut des optimisations pour fournir des continuations. Il montre comment plusieurs langages transpilés JS peuvent être automatiquement enrichis à l'aide de ces transformations.

Bien que j'aimerais toujours voir un meilleur support natif (voir nos chiffres de performance), Stopify offre un bon chemin (voir nos chiffres de performance) de la réalité d'aujourd'hui à ce rêve. En particulier, cela enlève une tonne de brut du système d'exécution, séparant les problèmes de langage de base des problèmes liés à la continuation.

CC @arjunguha @jpolitz @rachitnigam

Pour faire suite à certains de mes commentaires ci-dessus, je devrais probablement créer un lien vers ma présentation de la réunion de février 2020, montrant des idées pour ajouter des gestionnaires de commutation/coroutines/continuations/effets à Wasm .

Clôture en faveur du #1359

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

KloudKoder picture KloudKoder  ·  55Commentaires

JakeTrock picture JakeTrock  ·  44Commentaires

jfbastien picture jfbastien  ·  244Commentaires

jfbastien picture jfbastien  ·  80Commentaires

PoignardAzur picture PoignardAzur  ·  61Commentaires