Design: Veuillez prendre en charge les étiquettes arbitraires et les Gotos.

Créé le 8 sept. 2016  ·  159Commentaires  ·  Source: WebAssembly/design

Je tiens à souligner que je n'ai pas été impliqué dans l'effort d'assemblage Web,
et je ne maintiens aucun compilateur volumineux ou largement utilisé (juste le mien
langage jouet, des contributions mineures au backend du compilateur QBE et un
stage dans l'équipe de compilateurs d'IBM), mais j'ai fini par devenir un peu fantasque, et
a été encouragé à partager plus largement.

Alors, même si je suis un peu mal à l'aise de me lancer et de suggérer des changements majeurs
à un projet sur lequel je n'ai pas travaillé... voici :

Mes réclamations :

Quand j'écris un compilateur, la première chose que je ferais avec le haut niveau
structure -- boucles, instructions if, et ainsi de suite -- est de les valider pour la sémantique,
faire la vérification de type et ainsi de suite. La deuxième chose que je fais avec eux est juste de les jeter
dehors, et aplatir aux blocs de base, et éventuellement à la forme SSA. Dans d'autres parties
du monde des compilateurs, un format populaire est le style de passage de continuation. je ne suis pas
un expert de la compilation avec un style de passe de continuation, mais il ne semble pas non plus
être un bon ajustement pour les boucles et les blocs de portée que l'assemblage Web semble avoir
embrassé.

J'aimerais faire valoir qu'un format plus plat basé sur goto serait beaucoup plus utile car
une cible pour les développeurs de compilateurs, et n'entraverait pas de manière significative le
écriture d'un polyfill utilisable.

Personnellement, je ne suis pas non plus un grand fan des expressions complexes imbriquées. ils sont un peu
plus maladroit à consommer, surtout si les nœuds internes peuvent avoir des effets secondaires, mais je
ne vous y opposez pas fortement en tant qu'implémenteur de compilateur -- L'assembly Web
JIT peut les consommer, je peux les ignorer et générer les instructions qui mappent
à mon IR. Ils ne me donnent pas envie de renverser les tables.

Le plus gros problème se résume aux boucles, blocs et autres éléments syntaxiques
que, en tant qu'auteur de compilateur optimisant, vous vous efforcez de représenter comme un
graphique avec des branches représentant des arêtes ; Les constructions de flux de contrôle explicites
sont un obstacle. Les reconstruire à partir du graphique une fois que vous avez réellement terminé
les optimisations que vous souhaitez sont certes possibles, mais c'est pas mal
complexité pour contourner un format plus complexe. Et ça m'agace : tant les
le producteur et le consommateur travaillent autour de problèmes entièrement inventés
qui serait évité en supprimant simplement des constructions de flux de contrôle complexes
de l'assemblage Web.

De plus, l'insistance sur des construits de niveau supérieur conduit à certains
cas pathologiques. Par exemple, l'appareil de Duff se retrouve avec un site Web horrible
sortie de l'assembly, comme on le voit en déconnant dans
Cependant, l'inverse n'est pas vrai : tout ce qui peut être exprimé
dans l'assembleur Web peut être trivialement converti en un équivalent dans certains
format non structuré basé sur goto.

Donc, à tout le moins, j'aimerais suggérer que l'équipe d'assemblage Web ajoute
prise en charge des étiquettes et des gotos arbitraires. S'ils choisissent de garder le plus haut
constructions de niveau, ce serait un peu de complexité inutile, mais au moins
les auteurs de compilateurs comme moi pourraient les ignorer et générer une sortie
directement.

Polyremplissage :

L'une des préoccupations que j'ai entendues en discutant de cela est que la boucle
et la structure à base de blocs permet un remplissage plus facile de l'assemblage de la bande.
Bien que ce ne soit pas entièrement faux, je pense qu'une simple solution de polyfill
pour les étiquettes et les gotos est possible. Ce n'est peut-être pas aussi optimal,
Je pense que ça vaut un peu de laideur dans le bytecode dans l'ordre
pour éviter de démarrer un nouvel outil avec une dette technique intégrée.

Si nous supposons une syntaxe de type LLVM (ou QBE) pour l'assemblage Web, alors du code
ça ressemble à :

int f(int x) {
    if (x == 42)
        return 123;
    else
        return 666;
}

pourrait compiler pour :

 func @f(%x : i32) {
    %1 = test %x 42
jmp %1 iftrue iffalse

 L0:
    %r =i 123
jmp LRet
 L1:
    %r =i 666
jmp LRet
 Lret:
    ret %r
 }

Cela pourrait être polyrempli en Javascript qui ressemble à :

function f(x) {
    var __label = L0;
    var __ret;

    while (__label != LRet) {
        switch (__label) {
        case L0:
            var _v1 = (x == 42)
            if (_v1) {__lablel = L1;} else {label = L2;}
            break;
        case L1:
            __ret = 123
            __label = LRet
            break;
        case L2;
            __ret = 666
            __label = LRet
            break;
        default:
            assert(false);
            break;
    }
}

C'est moche ? Ouais. Est-ce que ça importe? Espérons que si l'assemblage Web décolle,
pas pour longtemps.

Et sinon:

Eh bien, si jamais je parvenais à cibler l'assemblage Web, je suppose que je générerais du code
en utilisant l'approche que j'ai mentionnée dans le polyfill, et faire de mon mieux pour ignorer tout
les constructions de haut niveau, en espérant que les compilateurs seraient assez intelligents pour
accrochez-vous à ce modèle.

Mais ce serait bien si nous n'avions pas besoin d'avoir les deux côtés de la génération de code
contourner le format spécifié.

control flow

Commentaire le plus utile

La prochaine version Go 1.11 aura un support expérimental pour WebAssembly. Cela inclura une prise en charge complète de toutes les fonctionnalités de Go, y compris les goroutines, les canaux, etc. Cependant, les performances du WebAssembly généré ne sont actuellement pas très bonnes.

Ceci est principalement dû à l'instruction goto manquante. Sans l'instruction goto, nous avons dû recourir à une boucle de niveau supérieur et à une table de saut dans chaque fonction. L'utilisation de l'algorithme relooper n'est pas une option pour nous, car lors du basculement entre les goroutines, nous devons pouvoir reprendre l'exécution à différents points d'une fonction. Le relooper ne peut pas aider avec cela, seule une instruction goto peut le faire.

C'est génial que WebAssembly en soit arrivé au point où il peut prendre en charge un langage comme Go. Mais pour être vraiment l'assembleur du Web, WebAssembly doit être aussi puissant que les autres langages d'assemblage. Go dispose d'un compilateur avancé capable d'émettre un assemblage très efficace pour un certain nombre d'autres plates-formes. C'est pourquoi je voudrais argumenter que c'est principalement une limitation de WebAssembly et non du compilateur Go qu'il n'est pas possible d'utiliser également ce compilateur pour émettre des assemblys efficaces pour le web.

Tous les 159 commentaires

@oridb Wasm est quelque peu optimisé pour que le consommateur puisse se convertir rapidement au format SSA, et la structure aide ici pour les modèles de code courants, donc la structure n'est pas nécessairement un fardeau pour le consommateur. Je ne suis pas d'accord avec votre affirmation selon laquelle « les deux côtés de la génération de code fonctionnent autour du format spécifié ». Wasm concerne essentiellement un consommateur mince et rapide, et si vous avez des propositions pour le rendre plus mince et plus rapide, cela pourrait être constructif.

Les blocs qui peuvent être commandés dans un DAG peuvent être exprimés dans les blocs et les branches wasm, comme dans votre exemple. La boucle de commutation est le style utilisé lorsque cela est nécessaire, et peut-être que les consommateurs pourraient faire quelques sauts de threads pour aider ici. Jetez peut-être un coup d'œil à binaryen qui pourrait faire une grande partie du travail pour le backend de votre compilateur.

Il y a eu d'autres demandes pour un support CFG plus général, et d'autres approches utilisant des boucles ont été mentionnées, mais peut-être que l'accent est actuellement mis ailleurs.

Je ne pense pas qu'il soit prévu de prendre en charge le "style de passage de continuité" explicitement dans l'encodage, mais il a été mentionné des arguments de blocage et de boucle (comme un lambda) et prenant en charge plusieurs valeurs (plusieurs arguments lambda) et ajoutant un Opérateur pick pour faciliter le référencement des définitions (les arguments lambda).

la structure aide ici pour les modèles de code communs

Je ne vois aucun modèle de code commun plus facile à représenter en termes de branches vers des étiquettes arbitraires, par rapport au sous-ensemble de boucles et de blocs restreint que l'assemblage Web applique. Je pourrais voir un avantage mineur s'il y avait une tentative pour que le code ressemble étroitement au code d'entrée pour certaines classes de langage, mais cela ne semble pas être un objectif - et les constructions sont un peu nues si elles étaient là pour

Les blocs qui peuvent être commandés dans un DAG peuvent être exprimés dans les blocs et les branches wasm, comme dans votre exemple.

Oui, ils peuvent l'être. Cependant, je préférerais fortement ne pas ajouter de travail supplémentaire pour déterminer ceux qui peuvent être représentés de cette façon, par rapport à ceux qui nécessitent un travail supplémentaire. De manière réaliste, je sauterais l'analyse supplémentaire et générerais toujours simplement le formulaire de boucle de commutation.

Encore une fois, mon argument n'est pas que les boucles et les blocs rendent les choses impossibles ; C'est que tout ce qu'ils peuvent faire est plus simple et plus facile à écrire pour une machine avec goto, goto_if et des étiquettes arbitraires et non structurées.

Jetez peut-être un coup d'œil à binaryen qui pourrait faire une grande partie du travail pour le backend de votre compilateur.

J'ai déjà un backend réparable dont je suis assez satisfait et je prévois d'amorcer entièrement l'ensemble du compilateur dans mon propre langage. Je préfère ne pas ajouter une dépendance supplémentaire assez importante simplement pour contourner l'utilisation forcée de boucles/blocs. Si j'utilise simplement des boucles de commutation, émettre le code est assez trivial. Si j'essaie d'utiliser efficacement les fonctionnalités présentes dans l'assemblage Web, au lieu de faire de mon mieux pour prétendre qu'elles n'existent pas, cela devient beaucoup plus désagréable.

Il y a eu d'autres demandes pour un support CFG plus général, et d'autres approches utilisant des boucles mentionnées, mais peut-être que la force est ailleurs à l'heure actuelle.

Je ne suis toujours pas convaincu que les boucles aient des avantages - tout ce qui peut être représenté avec une boucle peut être représenté avec un goto et une étiquette, et il existe des conversions rapides et bien connues vers SSA à partir de listes d'instructions plates.

En ce qui concerne CPS, je ne pense pas qu'il y ait besoin d'un support explicite - il est populaire dans les cercles FP car il est assez facile de convertir directement en assemblage et offre des avantages similaires à SSA en termes de raisonnement (http:// mlton.org/pipermail/mlton/2003-janvier/023054.html); Encore une fois, je ne suis pas un expert en la matière, mais d'après ce dont je me souviens, la continuation de l'invocation est réduite à une étiquette, quelques movs et un goto.

@oridb 'il existe des conversions rapides et bien connues vers SSA à partir de listes d'instructions plates'

Serait intéressant de savoir comment ils se comparent aux décodeurs wasm SSA, c'est la question importante?

Wasm utilise actuellement une pile de valeurs, et certains des avantages de celle-ci disparaîtraient sans la structure, cela nuirait aux performances du décodeur. Sans la pile de valeurs, le décodage SSA aurait également plus de travail, j'ai essayé un code de base de registre et le décodage était plus lent (je ne sais pas à quel point c'est important).

Souhaitez-vous conserver la pile de valeurs ou utiliser une conception basée sur les registres ? Si vous conservez la pile de valeurs, cela devient peut-être un clone CIL, et peut-être que les performances de wasm pourraient être comparées à CIL, est-ce que quelqu'un a réellement vérifié cela?

Souhaitez-vous conserver la pile de valeurs ou utiliser une conception basée sur les registres ?

En fait, je n'ai pas de sentiments forts à ce sujet. J'imagine que la compacité de l'encodage serait l'une des plus grandes préoccupations ; Une conception de registre peut ne pas s'en tirer si bien là-bas - ou elle peut s'avérer être compressée de manière fantastique sur gzip. Je ne sais pas vraiment par tête.

Les performances sont une autre préoccupation, même si je soupçonne qu'elles pourraient être moins importantes étant donné la possibilité de mettre en cache la sortie binaire, ainsi que le fait que le temps de téléchargement peut l'emporter sur le décodage par des ordres de grandeur.

Serait intéressant de savoir comment ils se comparent aux décodeurs wasm SSA, c'est la question importante?

Si vous décodez vers SSA, cela implique que vous feriez également une quantité raisonnable d'optimisation. Je serais curieux de comparer l'importance des performances de décodage en premier lieu. Mais, oui, c'est certainement une bonne question.

Merci pour vos questions et préoccupations.

Il convient de noter que de nombreux concepteurs et implémenteurs de
WebAssembly a de l'expérience dans les JIT industriels hautes performances, non seulement
pour JavaScript (V8, SpiderMonkey, Chakra et JavaScriptCore), mais aussi dans
LLVM et autres compilateurs. J'ai personnellement implémenté deux JIT pour Java
bytecode et je peux attester qu'une machine à pile avec des gotos sans restriction
introduit une certaine complexité dans le décodage, la vérification et la construction d'un
compilateur IR. En fait, il existe de nombreux modèles qui peuvent être exprimés en Java
bytecode qui provoquera des JIT hautes performances, incluant à la fois C1 et C2 dans
HotSpot pour simplement abandonner et reléguer le code à s'exécuter uniquement dans le
interprète. En revanche, construire un compilateur IR à partir de quelque chose comme un
AST à partir de JavaScript ou d'un autre langage est quelque chose que j'ai également fait. le
La structure supplémentaire d'un AST rend une partie de ce travail beaucoup plus simple.

La conception des constructions de flux de contrôle de WebAssembly simplifie les consommateurs en
permettant une vérification rapide et simple, une conversion facile en un seul passage au formulaire SSA
(même un graphe IR), des JIT monopasses efficaces, et (avec postorder et le
machine à empiler) interprétation sur place relativement simple. Structuré
contrôle rend impossible les graphes de flux de contrôle irréductibles, ce qui élimine
toute une classe de boîtiers d'angle désagréables pour les décodeurs et les compilateurs. Ça aussi
prépare bien le terrain pour la gestion des exceptions dans le bytecode WASM, pour lequel V8
développe déjà un prototype de concert avec la production
la mise en oeuvre.

Nous avons eu beaucoup de discussions internes entre les membres à ce sujet très
sujet, puisque, pour un bytecode, c'est une chose qui est très différente de
d'autres cibles au niveau de la machine. Cependant, ce n'est pas différent du ciblage
un langage source comme JavaScript (ce que de nombreux compilateurs font de nos jours) et
ne nécessite qu'une réorganisation mineure des blocs pour atteindre la structure. Là
sont des algorithmes connus pour ce faire, et des outils. Nous aimerions fournir quelques
une meilleure orientation pour les producteurs commençant par un CFG arbitraire pour
mieux communiquer cela. Pour les langues ciblant WASM directement depuis un AST
(ce qui est en fait quelque chose que V8 fait maintenant pour le code asm.js - directement
traduction d'un bytecode JavaScript AST en WASM), il n'y a pas de restructuration
étape nécessaire. Nous nous attendons à ce que ce soit le cas pour de nombreux outils linguistiques
à travers le spectre qui n'ont pas d'IR sophistiqués à l'intérieur.

Le jeu. 8 sept. 2016 à 9 h 53, Ori Bernstein [email protected]
a écrit:

Souhaitez-vous conserver la pile de valeurs ou utiliser une conception basée sur les registres ?

En fait, je n'ai pas de sentiments forts à ce sujet. j'imagine
la compacité de l'encodage serait l'une des plus grandes préoccupations ; Comme toi
mentionné, la performance en est une autre.

Il serait intéressant de savoir comment ils se comparent aux décodeurs wasm SSA, que
est la question importante?

Si vous décodez en SSA, cela implique que vous feriez également un
quantité raisonnable d'optimisation. Je serais curieux de comparer comment
des performances de décodage significatives sont en premier lieu. Mais, oui, c'est
certainement une bonne question.

-
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/WebAssembly/design/issues/796#issuecomment-245521009 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ALnq1Iz1nn4--NL32R9ev0JPKfEnDyvqks5qn77cgaJpZM4J3ofA
.

Merci @titzer , je soupçonnais que la structure de Wasm avait un objectif au-delà de la simple similitude avec asm.js. Je me demande cependant : le bytecode Java (et CIL) ne modélise pas directement les CFG ou la pile de valeurs, ils doivent être déduits par le JIT. Mais dans Wasm (surtout si des signatures de blocs sont ajoutées), le JIT peut facilement comprendre ce qui se passe avec la pile de valeurs et le flux de contrôle, alors je me demande si les CFG (ou le flux de contrôle irréductible en particulier) ont été modélisés explicitement comme les boucles et les blocs, cela pourrait-il éviter la plupart des cas de coin désagréables auxquels vous pensez ?

Il y a cette optimisation soignée que les interprètes utilisent qui repose sur un flux de contrôle irréductible pour améliorer la prédiction de branche...

@oridb

J'aimerais faire valoir qu'un format plus plat basé sur goto serait beaucoup plus utile car
une cible pour les développeurs de compilateurs

Je suis d'accord que les gotos sont très utiles pour de nombreux compilateurs. C'est pourquoi des outils comme Binaryen vous permettent de générer des CFG arbitraires avec gotos , et ils peuvent les convertir très rapidement et efficacement en WebAssembly pour vous.

Il peut être utile de considérer WebAssembly comme un élément optimisé pour les navigateurs (comme l' a souligné

En ce qui concerne le polyfilling avec le modèle while-switch que vous mentionnez : dans emscripten, nous avons commencé de cette façon avant de développer la méthode "relooper" pour recréer des boucles. Le modèle de commutation while est environ 4 fois plus lent en moyenne (mais dans certains cas, beaucoup moins ou plus, par exemple les petites boucles sont plus sensibles). Je suis d'accord avec vous qu'en théorie, les optimisations de saut de thread pourraient accélérer cela, mais les performances seront moins prévisibles car certaines machines virtuelles le feront mieux que d'autres. Il est également beaucoup plus grand en termes de taille de code.

Il peut être utile de considérer WebAssembly comme un élément optimisé pour les navigateurs (comme l' a souligné

Je ne suis toujours pas convaincu que cet aspect aura autant d'importance - encore une fois, je soupçonne que le coût de récupération du bytecode dominerait le délai que l'utilisateur voit, le deuxième coût le plus important étant les optimisations effectuées, et non l'analyse et la validation . Je suppose/espère également que le bytecode serait jeté et que la sortie compilée serait ce qui serait mis en cache, faisant de la compilation un coût ponctuel.

Mais si vous optimisiez la consommation du navigateur Web, pourquoi ne pas simplement définir l'assemblage Web comme SSA, ce qui me semble à la fois plus conforme à ce à quoi je m'attendais et moins d'effort pour « convertir » en SSA ?

Vous pouvez commencer à analyser et à compiler pendant le téléchargement, et certaines machines virtuelles peuvent ne pas effectuer une compilation complète à l'avance (elles peuvent simplement utiliser une base de référence simple par exemple). Ainsi, les temps de téléchargement et de compilation peuvent être plus courts que prévu et, par conséquent, l'analyse et la validation peuvent constituer un facteur important dans le délai total que l'utilisateur voit.

En ce qui concerne les représentations SSA, elles ont tendance à avoir de grandes tailles de code. SSA est idéal pour optimiser le code, mais pas pour sérialiser le code de manière compacte.

@oridb Voir le commentaire de @titzer 'La conception des constructions de flux de contrôle de WebAssembly simplifie les consommateurs en permettant une vérification simple et rapide, une conversion facile en

Une grande partie de l'efficacité de codage de wasm semble provenir de son optimisation pour le modèle de code commun dans lequel les définitions ont un usage unique et sont utilisées dans l'ordre de la pile. Je m'attends à ce qu'un codage SSA puisse le faire aussi, il pourrait donc être d'une efficacité de codage similaire. Les opérateurs tels que if_else pour les motifs en losanges sont également très utiles. Mais sans la structure wasm, il semble que tous les blocs de base auraient besoin de lire les définitions des registres et d'écrire les résultats dans les registres, et cela pourrait ne pas être aussi efficace. Par exemple, je pense que wasm peut faire encore mieux avec un opérateur pick qui pourrait référencer des valeurs de pile étendues en amont de la pile et au-delà des limites de blocs de base.

Je pense que wasm n'est pas très loin de pouvoir encoder la plupart du code dans le style SSA. Si les définitions étaient transmises dans l'arborescence de la portée en tant que sorties de bloc de base, elles pourraient être complètes. Le codage SSA pourrait-il être orthogonal à la matière CFG. Par exemple, il pourrait y avoir un codage SSA avec les restrictions wasm CFG, il pourrait y avoir une VM basée sur un registre avec les restrictions CFG.

Un objectif de wasm est de déplacer la charge d'optimisation sur le consommateur d'exécution. Il existe une forte résistance à l'ajout de complexité dans le compilateur d'exécution, car cela augmente la surface d'attaque. Une grande partie du défi de conception consiste à se demander ce qui peut être fait pour simplifier le compilateur d'exécution sans nuire aux performances, et beaucoup de débats !

Eh bien, il est probablement trop tard maintenant, mais j'aimerais remettre en question l'idée que l'algorithme relooper, ou ses variantes, puisse produire des résultats "assez bons" dans tous les cas. Ils le peuvent clairement dans la plupart des cas, puisque la plupart du code source ne contient pas de flux de contrôle irréductible pour commencer, les optimisations ne rendent généralement pas les choses trop compliquées, et si elles le font, par exemple dans le cadre de la fusion de blocs en double, elles peuvent probablement être enseignées pas à. Mais qu'en est-il des cas pathologiques ? Par exemple, que se passe-t-il si vous avez une coroutine qu'un compilateur a transformée en une fonction régulière avec une structure comme ce pseudo-C :

void transformed_coroutine(struct autogenerated_context_struct *ctx) {
    int arg1, arg2; // function args
    int var1, var2, var3, …; // all vars used by the function
    switch (ctx->current_label) { // restore state
    case 0:
        // initial state, load function args caller supplied and proceed to start
        arg1 = ctx->arg1;
        arg2 = ctx->arg2;
        break;
    case 1: 
        // restore all vars which are live at label 1, then jump there
        var2 = ctx->var2; 
        var3 = ctx->var3;
        goto resume_1;
    [more cases…]
    }

    [main body goes here...]
    [somewhere deep in nested control flow:]
        // originally a yield/await/etc.
        ctx->var2 = var2;
        ctx->var3 = var3;
        ctx->current_label = 1;
        return;
        resume_1:
        // continue on
}

Vous avez donc un flux de contrôle principalement normal, mais avec quelques gotos pointés au milieu. C'est à peu près comment fonctionnent les coroutines LLVM .

Je ne pense pas qu'il y ait un bon moyen de reboucler quelque chose comme ça, si le flux de contrôle "normal" est suffisamment complexe. (Cela peut être faux.) Soit vous dupliquez des parties massives de la fonction, nécessitant potentiellement une copie séparée pour chaque point de rendement, soit vous transformez le tout en un commutateur géant, qui selon @kripken est 4 fois plus lent que relooper sur le code typique ( ce qui lui-même est probablement un peu plus lent que de ne pas avoir besoin du tout de relooper).

La machine virtuelle pourrait réduire la surcharge d'un commutateur géant avec des optimisations de threads de saut, mais il est sûrement plus coûteux pour la machine virtuelle d'effectuer ces optimisations, essentiellement en devinant comment le code se réduit à des gotos, que d'accepter simplement des gotos explicites. Comme le dit @kripken , c'est aussi moins prévisible.

Peut-être que faire ce genre de transformation est une mauvaise idée pour commencer, car après rien ne domine rien, donc les optimisations basées sur SSA ne peuvent pas faire grand-chose… Mais le compilateur peut effectuer la plupart des optimisations avant d' effectuer la transformation, et il semble qu'au moins les concepteurs des coroutines LLVM n'aient pas vu un besoin urgent de retarder la transformation jusqu'à la génération du code. D'un autre côté, étant donné qu'il existe une grande variété dans la sémantique exacte que les gens attendent des coroutines (par exemple, duplication de coroutines suspendues, possibilité d'inspecter les « stack frames » pour GC), lorsqu'il s'agit de concevoir un bytecode portable (plutôt qu'un compilateur), il est plus flexible de prendre en charge correctement le code déjà transformé que de laisser la VM effectuer la transformation.

Quoi qu'il en soit, les coroutines ne sont qu'un exemple. Un autre exemple auquel je peux penser est la mise en œuvre d'une VM dans une VM. Alors qu'une caractéristique plus courante des JIT est les sorties latérales , qui ne nécessitent pas de goto, il existe des situations qui appellent des entrées latérales - encore une fois, nécessitant goto au milieu des boucles et autres. Un autre serait les interpréteurs optimisés : non pas que les interpréteurs ciblant wasm puissent vraiment correspondre à ceux qui ciblent le code natif, ce qui peut au minimum améliorer les performances avec les gotos calculés, et peut plonger dans l'assemblage pour plus… mais une partie de la motivation pour les gotos calculés est de mieux exploiter le prédicteur de branche en donnant à chaque cas sa propre instruction de saut, afin que vous puissiez reproduire une partie de l'effet en ayant un commutateur séparé après chaque gestionnaire d'opcode, où les cas seraient tous simplement des gotos. Ou au moins avoir un if ou deux pour vérifier les instructions spécifiques qui viennent généralement après l'actuelle. Il existe des cas particuliers de ce modèle qui pourraient être représentables avec un flux de contrôle structuré, mais pas le cas général. Etc…

Il existe sûrement un moyen d'autoriser un flux de contrôle arbitraire sans faire faire beaucoup de travail à la machine virtuelle. L'idée de l'homme de paille, pourrait être cassée : vous pourriez avoir un schéma où les sauts vers les portées enfants sont autorisés, mais seulement si le nombre de portées que vous devez entrer est inférieur à une limite définie par le bloc cible. La limite serait par défaut à 0 (pas de sauts depuis les étendues parents), ce qui préserve la sémantique actuelle, et la limite d'un bloc ne peut pas être supérieure à la limite du bloc parent + 1 (facile à vérifier). Et la VM modifierait son heuristique de dominance de « X domine Y s'il s'agit d'un parent de Y » en « X domine Y s'il s'agit d'un parent de Y avec une distance supérieure à la limite de saut enfant de Y ». (Il s'agit d'une approximation prudente, il n'est pas garanti qu'elle représente l'ensemble dominant exact, mais il en va de même pour l'heuristique existante - il est possible qu'un bloc interne domine la moitié inférieure d'un bloc externe.) Étant donné que seul le code avec un flux de contrôle irréductible aurait besoin de spécifier une limite, cela n'augmenterait pas la taille du code dans le cas courant.

Edit : Il est intéressant de noter que cela transformerait essentiellement la structure du bloc en une représentation de l'arbre de dominance. Je suppose qu'il serait beaucoup plus simple d'exprimer cela directement : un arbre de blocs de base, où un bloc est autorisé à passer à un frère, un ancêtre ou un bloc enfant immédiat, mais pas à un autre descendant. Je ne sais pas comment cela correspond le mieux à la structure de portée existante, où un "bloc" peut consister en plusieurs blocs de base avec des sous-boucles entre les deux.

FWIW : Wasm a une conception particulière, qui s'explique en quelques mots très significatifs "sauf que la restriction d'imbrication rend impossible le branchement au milieu d'une boucle depuis l'extérieur de la boucle".

S'il ne s'agissait que d'un DAG, la validation pourrait simplement vérifier que les branches étaient en avant, mais avec des boucles, cela permettrait de se brancher au milieu de la boucle depuis l'extérieur de la boucle, d'où la conception de blocs imbriqués.

Le CFG n'est qu'une partie de cette conception, l'autre étant le flux de données, et il existe une pile de valeurs et des blocs peuvent également être organisés pour dérouler la pile de valeurs, ce qui peut très utilement communiquer la plage en direct au consommateur, ce qui économise le travail de conversion en SSA .

Il est possible d'étendre wasm pour qu'il soit un encodage SSA (ajouter pick , autoriser les blocs à renvoyer plusieurs valeurs et avoir des valeurs pop d'entrées de boucle), il est donc intéressant de noter que les contraintes requises pour un décodage SSA efficace pourraient ne pas être nécessaires (parce que il pourrait déjà être encodé en SSA) ! Cela conduit à un langage fonctionnel (qui pourrait avoir un codage de style pile pour plus d'efficacité).

Si cela était étendu pour gérer les CFG arbitraires, cela pourrait ressembler à ce qui suit. Il s'agit d'un codage de style SSA, les valeurs sont donc des constantes. Il semble toujours correspondre dans une large mesure au style de pile, mais pas certain de tous les détails. Ainsi, dans blocks branches pourraient être créées vers n'importe quel autre bloc étiqueté de cet ensemble, ou une autre convention utilisée pour transférer le contrôle à un autre bloc. Le code dans le bloc peut toujours référencer utilement des valeurs sur la pile de valeurs plus haut dans la pile pour éviter de toutes les transmettre.

(func f1 (arg1)
  (let ((c1 10)) ; Some values up the stack.
    (blocks ((b1 (a1 a2 a3)
                   ... (br b3)
               (br b2 (+ a1 a2 a3 arg1 c1)))
             (b2 (a1)
                 ... (br b1 ...))
             (b3 ()
                 ...))
   .. regular structured wasm ..
   (br b2 ...)
   ....
   (br b3)
    ...
   ))

Mais les navigateurs Web géreraient-ils jamais cela efficacement en interne ?

Est-ce que quelqu'un ayant une formation en machine à pile reconnaîtrait le modèle de code et serait capable de le faire correspondre à un codage de pile ?

Il y a une discussion intéressante sur les boucles irréductibles ici http://bboissin.appspot.com/static/upload/bboissin-thesis-2010-09-22.pdf

Je n'ai pas tout suivi sur un passage rapide, mais il mentionne la conversion de boucles irréductibles en boucles réductibles en ajoutant un nœud d'entrée. Pour wasm, cela ressemble à l'ajout d'une entrée définie aux boucles qui est spécifiquement destinée à être répartie dans la boucle, similaire à la solution actuelle mais avec une variable définie pour cela. Les mentions ci-dessus sont virtualisées, optimisées, dans le traitement. Peut-être que quelque chose comme ça pourrait être une option?

Si cela est à l'horizon, et étant donné que les producteurs ont déjà besoin d'utiliser une technique similaire mais en utilisant une variable locale, cela vaut-il la peine d'envisager maintenant afin que le produit produit tôt ait le potentiel de fonctionner plus rapidement sur des temps d'exécution plus avancés ? Cela pourrait également créer une incitation à la concurrence entre les environnements d'exécution pour explorer cela.

Il ne s'agirait pas exactement d'étiquettes et de gotos arbitraires, mais de quelque chose en quoi ceux-ci pourraient être transformés et qui a une certaine chance d'être efficacement compilé à l'avenir.

Pour mémoire, je suis fortement avec @oridb et @comex sur cette question.
Je pense qu'il s'agit d'une question cruciale qui devrait être abordée avant qu'il ne soit trop tard.

Étant donné la nature de WebAssembly, toutes les erreurs que vous faites maintenant sont susceptibles de persister pendant des décennies (regardez Javascript !). C'est pourquoi la question est si critique ; évitez de prendre en charge les gotos maintenant pour quelque raison que ce soit (par exemple pour faciliter l'optimisation, qui est --- très franchement --- l'influence d'une implémentation spécifique sur une chose générique, et honnêtement, je pense que c'est paresseux), et vous vous retrouverez avec problèmes à long terme.

Je peux déjà voir des implémentations WebAssembly futures (ou actuelles, mais à l'avenir) essayant de reconnaître des cas particuliers les modèles while/switch habituels pour implémenter des étiquettes afin de les gérer correctement. C'est un hack.

Est WebAssembly table rase, donc il est temps pour éviter hacks sales (ou plutôt, les exigences pour eux).

@darkuranium :

WebAssembly tel qu'il est actuellement spécifié est déjà livré dans les navigateurs et les chaînes d'outils, et les développeurs ont déjà créé du code qui prend la forme présentée dans cette conception. On ne peut donc

Nous pouvons, cependant, ajouter à la conception d'une manière rétrocompatible. Je ne pense pas qu'aucune des personnes impliquées ne pense que goto soit inutile. Je soupçonne que nous utilisons tous régulièrement goto , et pas seulement dans des manières de jouet syntaxique.

À ce stade, une personne motivée doit faire une proposition qui a du sens et la mettre en œuvre. Je ne vois pas une telle proposition rejetée si elle fournit des données solides.

Étant donné la nature de WebAssembly, toutes les erreurs que vous faites maintenant sont susceptibles de persister pendant des décennies (regardez Javascript !). C'est pourquoi la question est si critique ; évitez de prendre en charge les gotos maintenant pour quelque raison que ce soit (par exemple pour faciliter l'optimisation, qui est --- très franchement --- l'influence d'une implémentation spécifique sur une chose générique, et honnêtement, je pense que c'est paresseux), et vous vous retrouverez avec problèmes à long terme.

Je vais donc appeler votre bluff : je pense qu'avoir la motivation dont vous faites preuve, et ne pas faire de proposition et de mise en œuvre comme je le détaille ci-dessus, est franchement paresseux.

Je suis insolent bien sûr. Considérez que nous avons des gens qui frappent à nos portes pour des threads, GC, SIMD, etc. Il y a des gens qui le font pour les autres fonctionnalités que je mentionne. Aucun pour goto jusqu'à présent. Veuillez vous familiariser avec les directives de contribution de ce groupe et vous amuser.

Sinon, je pense que goto est une excellente future fonctionnalité . Personnellement, j'aborderais probablement les autres en premier, comme la génération de code JIT. C'est mon intérêt personnel après GC et les discussions.

Salut. Je suis en train d'écrire une traduction de webassembly vers IR et de nouveau vers webassembly, et j'ai eu une discussion sur ce sujet avec des gens.

On m'a fait remarquer qu'un flux de contrôle irréductible est difficile à représenter dans l'assemblage Web. Cela peut s'avérer gênant pour l'optimisation des compilateurs qui écrivent occasionnellement des flux de contrôle irréductibles. Cela pourrait être quelque chose comme la boucle ci-dessous, qui a plusieurs points d'entrée :

if (x) goto inside_loop;
// banana
while(y) {
    // things
    inside_loop:
    // do things
}

Les compilateurs EBB produiraient ce qui suit :

entry:
    cjump x, inside_loop
    // banana
    jump loop

loop:
    cjump y, exit
    // things
    jump inside_loop

inside_loop:
    // do things
    jump loop
exit:
    return

Ensuite, nous arrivons à traduire cela en webassembly. Le problème est que bien que nous ayons des décompilateurs compris il y a longtemps , ils avaient toujours la possibilité d'ajouter le goto dans des flux irréductibles.

Avant qu'il ne soit traduit, le compilateur va faire des trucs à ce sujet. Mais finalement, vous parcourez le code et positionnez les débuts et les fins des structures. Vous vous retrouvez avec les candidats suivants après avoir éliminé les sauts de chute :

<inside_loop, if(x)>
    // banana
<loop °>
<exit if(y)>
    // things
</inside_loop, if(x)>
    // do things
</loop ↑>
</exit>

Ensuite, vous devez créer une pile à partir de ceux-ci. Lequel va en bas ? C'est soit la 'boucle intérieure' ou alors c'est la 'boucle'. Nous ne pouvons pas faire cela, nous devons donc couper la pile et copier les choses :

if
    // do things
else
    // banana
end
loop
  br out
    // things
    // do things
end

Nous pouvons maintenant traduire cela en webassembly. Excusez-moi, je ne sais pas encore comment ces boucles se construisent.

Ce n'est pas un problème particulier si l'on pense aux anciens logiciels. Il est probable que le nouveau logiciel soit traduit en assemblage Web. Mais le problème réside dans le fonctionnement de nos compilateurs. Ils effectuent le contrôle du flux avec des blocs de base depuis des décennies et supposent que tout se passe bien.

Techniquement, la langue est traduite, puis traduite. Nous n'avons besoin que d'un mécanisme qui permette aux valeurs de traverser les frontières sans drame. Le flux structuré n'est utile que pour les personnes ayant l'intention de lire le code.

Mais par exemple, ce qui suit fonctionnerait tout aussi bien :

    cjump x, label(1)
    // banana
0: label
    cjump y, label(2)
    // things
1: label
    // do things
    jump label(0)
2: label
    // exit as usual, picking the values from the top of the stack.

Les nombres seraient implicites, c'est-à-dire que lorsque le compilateur voit une 'étiquette', il sait qu'il démarre un nouveau bloc étendu et lui donne un nouveau numéro d'index, commençant à s'incrémenter à partir de 0.

Pour produire une pile statique, vous pouvez suivre le nombre d'éléments dans la pile lorsque vous rencontrez un saut dans l'étiquette. S'il y a une pile incohérente après un saut dans l'étiquette, le programme est invalide.

Si vous trouvez ce qui précède mauvais, vous pouvez également essayer d'ajouter une longueur de pile explicite dans chaque étiquette (peut-être un delta par rapport à la taille de pile de la dernière étiquette indexée, si la valeur absolue est mauvaise pour la compression), et un marqueur à chaque saut sur le nombre de valeurs il copie depuis le haut de la pile pendant le saut.

Je pourrais parier que vous ne pouvez en aucun cas déjouer le gzip par le fait que vous représentez le flux de contrôle, vous pouvez donc choisir le flux qui convient aux gars qui ont le plus de travail ici. (Je peux illustrer avec ma chaîne d'outils de compilateur flexible pour « déjouer le gzip » si vous le souhaitez, envoyez-moi simplement un message et mettons en place une démo !)

Je me sens comme une tête brisée en ce moment. Il suffit de relire la spécification WebAssembly et de constater que le flux de contrôle irréductible est intentionnellement exclu du MVP, peut-être pour la raison qu'emscripten a dû résoudre le problème dès les premiers jours.

La solution sur la façon de gérer le flux de contrôle irréductible dans WebAssembly est expliquée dans l'article "Emscripten: An LLVM-to-JavaScript Compiler". Le relooper réorganise le programme quelque chose comme ceci :

_b_ = bool(x)
_b_ == 0 if
  // banana
end
block loop
  _b_ if
    // do things
    _b_ = 0
  else
    y br_if 2
    // things
    _b_ = 1
  end
  br 0
end end

Le rationnel était que le flux de contrôle structuré aide à lire le vidage du code source, et je suppose qu'il est censé aider les implémentations de polyfill.

Les personnes qui compilent à partir de l'assemblage Web s'adapteront probablement pour gérer et séparer le flux de contrôle réduit.

Alors:

  • Comme mentionné, WebAssembly est maintenant stable, le temps est donc révolu pour toute réécriture totale de la façon dont le flux de contrôle est exprimé.

    • Dans un sens, c'est malheureux, car personne n'a réellement testé si un codage plus directement basé sur SSA aurait pu atteindre la même compacité que la conception actuelle.

    • Cependant, lorsqu'il s'agit de spécifier goto, cela rend le travail beaucoup plus facile ! Les instructions basées sur des blocs dépassent déjà le bikeshedding, et ce n'est pas grave de s'attendre à ce que les compilateurs de production ciblant wasm expriment un flux de contrôle réductible en les utilisant - l'algorithme n'est pas si difficile. Le problème principal est qu'une petite fraction du flux de contrôle ne peut pas être exprimée en les utilisant sans un coût de performance. Si nous résolvons ce problème en ajoutant une nouvelle instruction goto, nous n'avons pas à nous soucier autant de l'efficacité de l'encodage que nous le ferions avec une refonte totale. Le code utilisant goto devrait toujours être raisonnablement compact, bien sûr, mais il n'a pas à rivaliser avec d'autres constructions pour la compacité ; c'est uniquement pour le flux de contrôle irréductible et doit être utilisé rarement.

  • La réductibilité n'est pas particulièrement utile.

    • La plupart des backends de compilateur utilisent une représentation SSA basée sur un graphique de blocs de base et de branches entre eux. La structure de boucle imbriquée, ce que garantit la réductibilité, est à peu près rejetée au début.

    • J'ai vérifié les implémentations actuelles de WebAssembly dans JavaScriptCore, V8 et SpiderMonkey, et elles semblent toutes suivre ce modèle. (V8 est plus compliqué - une sorte de représentation "de la mer de nœuds" plutôt que des blocs de base - mais jette également la structure d'imbrication.)

    • Exception : l'analyse de boucle peut être utile, et ces trois implémentations transmettent des informations à l'IR sur les blocs de base qui sont les débuts des boucles. (Comparez à LLVM qui, en tant que backend « lourd » conçu pour la compilation AOT, le jette et le recalcule dans le backend. C'est plus robuste, car il peut trouver des choses qui ne ressemblent pas à des boucles dans le code source mais qui le font après un tas d'optimisations, mais plus lentement.)

    • L'analyse de boucle fonctionne sur des "boucles naturelles", qui interdisent les branches au milieu de la boucle qui ne passent pas par l'en-tête de boucle.

    • WebAssembly doit continuer à garantir que les blocs loop sont des boucles naturelles.

    • Mais l'analyse de boucle n'exige pas que toute la fonction soit réductible, ni même l'intérieur de la boucle : elle interdit simplement les branchements de l'extérieur vers l'intérieur. La représentation de base est toujours un graphe de flux de contrôle arbitraire.

    • Un flux de contrôle irréductible rend plus difficile la compilation de WebAssembly en JavaScript (polyfilling), car le compilateur devrait exécuter lui-même l'algorithme relooper.

    • Mais WebAssembly prend déjà plusieurs décisions qui ajoutent une surcharge d'exécution importante à toute approche de compilation vers JS (y compris la prise en charge des accès mémoire non alignés et le piégeage des accès hors limites), suggérant que cela n'est pas considéré comme très important.

    • Comparé à cela, rendre le compilateur un peu plus complexe n'est pas un gros problème.

    • Par conséquent, je ne pense pas qu'il y ait une bonne raison de ne pas ajouter une sorte de support pour un flux de contrôle irréductible.

  • La principale information nécessaire pour construire une représentation SSA (qui, par conception, devrait être possible en une seule passe) est l' arbre dominant .

    • Actuellement, un backend peut estimer la dominance sur la base d'un flux de contrôle structuré. Si je comprends bien la spécification, les instructions suivantes terminent un bloc de base :

    • block :



      • Le BB commençant le bloc est dominé par le BB précédent.*


      • Le BB suivant le end est dominé par le BB commençant le bloc, mais pas par le BB avant end (car il sera ignoré s'il y avait un br out ).



    • loop :



      • Le BB commençant le bloc est dominé par le BB précédent.


      • Le BB après end est dominé par le BB avant end (puisque vous ne pouvez pas accéder à l'instruction après end sauf en exécutant end ).



    • if :



      • Le côté if, le côté else et le BB après end sont tous dominés par le BB avant if .



    • br , return , unreachable :



      • (Le BB immédiatement après br , return , ou unreachable est inaccessible.)



    • br_if , br_table :



      • Le BB avant br_if / br_table domine celui qui le suit.



    • Notamment, il ne s'agit que d'une estimation. Il ne peut pas produire de faux positifs (dire que A domine B alors qu'en réalité ce n'est pas le cas) car il ne le dit que lorsqu'il n'y a aucun moyen d'accéder à B sans passer par A, par construction. Mais cela peut produire de faux négatifs (dire que A ne domine pas B alors qu'il le fait réellement), et je ne pense pas qu'un algorithme à un seul passage puisse les détecter (cela pourrait être faux).

    • Exemple de faux négatif :

      ```

      bloquer $extérieur

      boucler

      br $extérieur ;; puisque cela casse inconditionnellement, il domine secrètement la fin BB

      finir

      finir

    • Mais ce n'est pas grave, autant que je sache.



      • Les faux positifs seraient mauvais, car par exemple, si le bloc de base A est censé dominer le bloc de base B, le code machine pour B peut utiliser un registre défini dans A (si rien entre les deux n'écrase ce registre). Si A ne domine pas réellement B, le registre peut avoir une valeur d'ordures.


      • Les faux négatifs sont essentiellement des branches fantômes qui ne se produisent jamais. Le compilateur suppose que ces branches pourraient se produire, mais pas qu'elles doivent le faire, donc le code généré est juste plus conservateur que nécessaire.



    • Quoi qu'il en soit, réfléchissez à la façon dont une instruction goto devrait fonctionner en termes d'arbre dominant. Supposons que A domine B, qui domine C.

    • Nous ne pouvons pas sauter de A à C car cela sauterait B (violant l'hypothèse de dominance). En d'autres termes, nous ne pouvons pas sauter aux descendants non immédiats. (Et du côté du producteur binaire, s'il calcule le véritable arbre dominant, il n'y aura jamais un tel saut.)

    • Nous pourrions sauter en toute sécurité de A à B, mais aller à un descendant immédiat n'est pas très utile. C'est fondamentalement équivalent à une instruction if ou switch, que nous pouvons déjà faire (en utilisant l'instruction if s'il n'y a qu'un test binaire, ou br_table s'il y en a plusieurs).

    • Sauter vers un frère ou un frère d'un ancêtre est également sûr et plus intéressant. Si nous sautons sur notre frère, nous avons conservé la garantie que notre parent domine notre frère, car nous devons déjà avoir exécuté notre parent pour arriver ici (puisqu'il nous domine aussi). De même pour les ancêtres.

    • En général, un binaire malveillant pourrait produire de faux négatifs en dominance de cette façon, mais comme je l'ai dit, ceux-ci sont (a) déjà possibles et (b) acceptables.

  • Sur cette base, voici une proposition d'homme de paille :

    • Une nouvelle instruction de type bloc :
    • labels type de résultat N instr* fin
    • Il doit y avoir exactement N instructions enfants immédiates, où "enfant immédiat" signifie soit une instruction de type bloc ( loop , block , ou labels ) et tout jusqu'au correspondant end , ou une seule instruction non bloquante (qui ne doit pas affecter la pile).
    • Au lieu de créer une seule étiquette comme les autres instructions de type bloc, labels crée N+1 étiquettes : N pointant vers les N enfants et une pointant vers la fin du bloc labels . Dans chacun des enfants, les indices d'étiquette 0 à N-1 se réfèrent aux enfants, dans l'ordre, et l'indice d'étiquette N se réfère à la fin.

    En d'autres termes, si vous avez
    loop ;; outer labels 3 block ;; child 0 br X end nop ;; child 1 nop ;; child 2 end end

    Selon X, le br fait référence à :

    | X | Cible |
    | ---------- | ------ |
    | 0 | fin du block |
    | 1 | enfant 0 (début du block ) |
    | 2 | enfant 1 (non) |
    | 3 | enfant 2 (non) |
    | 4 | fin de labels |
    | 5 | début de la boucle externe |

    • L'exécution commence au premier enfant.

    • Si l'exécution atteint la fin de l'un des enfants, elle passe au suivant. S'il atteint la fin du dernier enfant, il revient au premier enfant. (C'est pour la symétrie, car l'ordre des enfants n'est pas censé être significatif.)

    • Le branchement à l'un des enfants déroule la pile d'opérandes à sa profondeur au début de labels .

    • Il en va de même pour le branchement à la fin, mais si le type de résultat n'est pas vide, le branchement à la fin fait apparaître un opérande et le pousse après le déroulement, similaire à block .

    • Dominance : Le bloc de base avant l'instruction labels domine chacun des enfants, ainsi que le BB après la fin de labels . Les enfants ne se dominent pas ni à la fin.

    • Remarques sur la conception :

    • N est spécifié à l'avance afin que le code puisse être validé en un seul passage. Ce serait bizarre de devoir aller à la fin du bloc labels , pour connaître le nombre d'enfants, avant de connaître les cibles des indices qu'il contient.

    • Je ne sais pas s'il devrait éventuellement y avoir un moyen de transmettre des valeurs sur la pile d'opérandes entre les étiquettes, mais par analogie avec l'impossibilité de transmettre des valeurs dans un block ou loop , cela peut ne pas être pris en charge pour démarrer avec.

Ce serait vraiment bien s'il était possible de sauter dans une boucle, n'est-ce pas ? IIUC, si ce cas était pris en compte, le méchant combo boucle + br_table ne serait jamais nécessaire ...

Edit : oh, vous pouvez faire des boucles sans loop en sautant vers le haut dans labels . Je ne peux pas croire que j'ai raté ça.

@qwertie Si une boucle donnée n'est pas une boucle naturelle, le compilateur ciblant wasm doit l'exprimer en utilisant labels au lieu de loop . Il ne devrait jamais être nécessaire d'ajouter un commutateur pour exprimer le flux de contrôle, si c'est à cela que vous faites référence. (Après tout, au pire, vous pouvez simplement utiliser un bloc labels géant avec une étiquette pour chaque bloc de base de la fonction. Cela ne permet pas au compilateur de connaître la dominance et les boucles naturelles, vous risquez donc de manquer optimisations. Mais labels n'est requis que dans les cas où ces optimisations ne sont pas applicables.)

La structure de boucle imbriquée, ce que garantit la réductibilité, est à peu près rejetée au début. [...] J'ai vérifié les implémentations actuelles de WebAssembly dans JavaScriptCore, V8 et SpiderMonkey, et elles semblent toutes suivre ce modèle.

Pas tout à fait : au moins dans SM, le graphe IR n'est pas un graphe entièrement général ; nous supposons certains invariants de graphe qui découlent d'être générés à partir d'une source structurée (JS ou wasm) et simplifient et/ou optimisent souvent les algorithmes. La prise en charge d'un CFG entièrement général nécessiterait soit d'auditer/changer de nombreuses passes dans le pipeline pour ne pas assumer ces invariants (soit en les généralisant ou en les pessimisant en cas d'irréductibilité) ou de dupliquer les nœuds en amont pour rendre le graphe réductible. C'est certainement faisable, bien sûr, mais il n'est pas vrai qu'il s'agisse simplement d'un goulot d'étranglement artificiel.

De plus, le fait qu'il existe de nombreuses options et que différents moteurs feront des choses différentes suggère que le fait que le producteur traite l'irréductibilité dès le départ produira des performances un peu plus prévisibles en présence d'un flux de contrôle irréductible.

Lorsque nous avons discuté des chemins rétrocompatibles pour étendre wasm avec une prise en charge arbitraire de goto dans le passé, une grande question est de savoir quel est le cas d'utilisation ici : est-ce « simplifier les producteurs en n'ayant pas à exécuter un algorithme de type relooper » ou est-ce « permettre un codegen plus efficace pour un flux de contrôle réellement irréductible » ? Si ce n'est que le premier, alors je pense que nous voudrions probablement un schéma d'intégration d'étiquettes/gotos arbitraires (qui soit à la fois rétrocompatible et compose également avec les futurs try/catch structurés par blocs) ; il s'agit simplement de peser le rapport coût/bénéfice et les problèmes mentionnés ci-dessus.

Mais pour ce dernier cas d'utilisation, une chose que nous avons observée est que, alors que vous voyez de temps en temps le boîtier d'un appareil Duff dans la nature (ce qui n'est pas réellement un moyen efficace de dérouler une boucle...), souvent où vous voyez apparaître l'irréductibilité là où la performance compte, ce sont les boucles d'interprétation. Les boucles d'interprétation bénéficient également du threading indirect qui nécessite un goto calculé. De plus, même dans les compilateurs hors ligne costauds, les boucles d'interpréteur ont tendance à obtenir la pire allocation de registre. Étant donné que les performances de la boucle de l'interpréteur peuvent être assez importantes, une question est de savoir si nous avons vraiment besoin d'une primitive de flux de contrôle qui permet au moteur d'effectuer un threading indirect et de faire un regalloc décent. (C'est une question ouverte pour moi.)

@lukewagner
J'aimerais entendre plus de détails sur les passes qui dépendent des invariants. La conception que j'ai proposée, utilisant une construction distincte pour le flux irréductible, devrait permettre aux passes d'optimisation comme LICM d'éviter ce flux relativement facilement. Mais s'il y a d'autres types de casse auxquels je ne pense pas, j'aimerais mieux comprendre leur nature afin de pouvoir mieux savoir si et comment ils peuvent être évités.

Lorsque nous avons discuté des chemins rétrocompatibles pour étendre wasm avec une prise en charge arbitraire de goto dans le passé, une grande question est de savoir quel est le cas d'utilisation ici : est-ce « simplifier les producteurs en n'ayant pas à exécuter un algorithme de type relooper » ou est-ce « permettre un codegen plus efficace pour un flux de contrôle réellement irréductible » ?

Pour moi c'est le dernier; ma proposition s'attend à ce que les producteurs exécutent toujours un algorithme de type relooper pour sauver le backend du travail d'identification des dominants et des boucles naturelles, en ne revenant à labels que lorsque cela est nécessaire. Cependant, cela simplifierait encore les producteurs. Si un flux de contrôle irréductible a une pénalité importante, un producteur idéal devrait travailler très dur pour l'éviter, en utilisant des heuristiques pour déterminer s'il est plus efficace de dupliquer du code, la quantité minimale de duplication qui peut fonctionner, etc. Si la seule pénalité est potentiellement de donner optimisations de boucle ascendante, ce n'est pas vraiment nécessaire, ou du moins n'est pas plus nécessaire qu'il ne le serait avec un backend de code machine ordinaire (qui a ses propres optimisations de boucle).

Je devrais vraiment rassembler plus de données sur la façon dont le flux de contrôle irréductible est courant dans la pratique…

Cependant, ma conviction est que pénaliser un tel flux est essentiellement arbitraire et inutile. Dans la plupart des cas, l'effet sur le temps d'exécution global du programme devrait être faible. Cependant, s'il se trouve qu'un hotspot inclut un flux de contrôle irréductible, il y aura une pénalité sévère ; à l'avenir, les guides d'optimisation WebAssembly pourraient inclure cela comme piège commun et expliquer comment l'identifier et l'éviter. Si ma conviction est correcte, il s'agit d'une forme de surcharge cognitive totalement inutile pour les programmeurs. Et même lorsque la surcharge est faible, WebAssembly a déjà suffisamment de surcharge par rapport au code natif pour éviter tout excès.

Je suis ouvert à la persuasion que ma croyance est incorrecte.

Étant donné que les performances de la boucle de l'interpréteur peuvent être assez importantes, une question est de savoir si nous avons vraiment besoin d'une primitive de flux de contrôle qui permet au moteur d'effectuer un threading indirect et de faire un regalloc décent.

Cela semble intéressant, mais je pense qu'il serait préférable de commencer par une primitive plus générale. Après tout, une primitive conçue pour les interprètes nécessiterait toujours des backends pour gérer un flux de contrôle irréductible ; si vous allez mordre cette balle, autant soutenir le cas général aussi.

Alternativement, ma proposition pourrait déjà servir de primitive décente pour les interprètes. Si vous combinez labels avec br_table , vous avez la possibilité de pointer une table de saut directement à des points arbitraires de la fonction, ce qui n'est pas si différent d'un goto calculé. (Par opposition à un commutateur C, qui dirige au moins initialement le flux de contrôle vers des points à l'intérieur du bloc de commutateurs ; si les cas sont tous des gotos, le compilateur devrait être en mesure d'optimiser le saut supplémentaire, mais il peut également fusionner plusieurs « redondants » basculer les instructions en une seule, ruinant l'avantage d'avoir un saut séparé après chaque gestionnaire d'instructions.) Je ne sais pas quel est le problème avec l'allocation des registres, cependant ...

@comex Je suppose que l'on pourrait simplement désactiver toutes les passes d'optimisation au niveau de la fonction en présence d'un flux de contrôle irréductible (bien que la génération SSA, regalloc et probablement quelques autres soient nécessaires et nécessitent donc du travail), mais je supposais que nous voulait réellement générer du code de qualité pour des fonctions à flux de contrôle irréductible et cela implique d'auditer chaque algorithme qui supposait auparavant un graphe structuré.

>

La structure de boucle imbriquée, ce que garantit la réductibilité, est
à peu près jeté au début. [...] j'ai vérifié le courant
Implémentations WebAssembly dans JavaScriptCore, V8 et SpiderMonkey, et
ils semblent tous suivre ce modèle.

Pas tout à fait : au moins dans SM, le graphe IR n'est pas un graphe entièrement général ; nous
supposer certains invariants de graphe qui découlent d'être générés à partir d'un
source structurée (JS ou wasm) et souvent simplifier et/ou optimiser le
algorithmes.

Idem en V8. C'est en fait l'un de mes principaux reproches à l'égard de l'ASS dans les deux
la littérature et les implémentations respectives qu'ils ne définissent presque jamais
ce qui constitue un CFG « bien formé », mais ont tendance à supposer implicitement divers
contraintes non documentées de toute façon, généralement assurées par la construction par le
interface de langage. Je parie que beaucoup/la plupart des optimisations dans les compilateurs existants
ne serait pas en mesure de traiter des CFG vraiment arbitraires.

Comme le dit @lukewagner , le principal cas d'utilisation du contrôle irréductible est probablement
« code fileté » pour des interpréteurs optimisés. Difficile de dire à quel point ces
sont pour le domaine Wasm, et si son absence est réellement la plus grande
goulot.

Après avoir discuté du flux de contrôle irréductible avec un certain nombre de personnes
recherchant les IR du compilateur, la solution "la plus propre" serait probablement d'ajouter
la notion de blocs mutuellement récursifs. Cela correspondrait à celui de Wasm
structure de contrôle assez bien.

Les optimisations de boucle dans LLVM ignoreront généralement le flux de contrôle irréductible et ne tenteront pas de l'optimiser. L'analyse de boucle sur laquelle ils sont basés ne reconnaîtra que les boucles naturelles, vous devez donc être conscient qu'il peut y avoir des cycles CFG qui ne sont pas reconnus comme des boucles. Bien sûr, d'autres optimisations sont de nature plus locale et fonctionnent très bien avec des CFG irréductibles.

De mémoire, et probablement faux, SPEC2006 a une seule boucle irréductible dans 401.bzip2 et c'est tout. C'est assez rare en pratique.

Clang n'émettra qu'une seule instruction indirectbr dans les fonctions utilisant goto calculé. Cela a pour effet de transformer les interpréteurs threadés en boucles naturelles avec le bloc indirectbr comme en-tête de boucle. Après avoir quitté LLVM IR, le seul indirectbr est dupliqué en queue dans le générateur de code pour reconstruire l'enchevêtrement d'origine.

Il n'y a pas d'algorithme de vérification en un seul passage pour un flux de contrôle irréductible
dont je suis au courant. Le choix de conception pour le flux de contrôle réductible uniquement a été
fortement influencé par cette exigence.

Comme mentionné précédemment, le flot de contrôle irréductible peut être modélisé au moins deux
différentes façons. Une boucle avec une instruction switch peut en fait être optimisée
dans le graphe irréductible d'origine par un simple saut de filetage local
optimisation (par exemple en pliant le motif où une affectation d'une constante
à une variable locale se produit, puis un branchement à un branchement conditionnel qui
active immédiatement cette variable locale).

Ainsi, les constructions de contrôle irréductibles ne sont pas du tout nécessaires, et il est
seulement une question d'une seule transformation backend du compilateur pour récupérer le
graphe irréductible original et l'optimiser (pour les moteurs dont les compilateurs
prend en charge un flux de contrôle irréductible - ce qu'aucun des 4 navigateurs ne fait, au
meilleur de ma connaissance).

Meilleur,
-Ben

Le jeu. 20 avril 2017 à 05:20, Jakob Stoklund Olesen <
[email protected]> a écrit :

Les optimisations de boucle dans LLVM ignoreront généralement le flux de contrôle irréductible
et ne pas essayer de l'optimiser. L'analyse de boucle sur laquelle ils sont basés sera
ne reconnaissent que les boucles naturelles, vous devez donc être conscient qu'il peut
être des cycles CFG qui ne sont pas reconnus comme des boucles. Bien sûr, d'autres
les optimisations sont de nature plus locale et fonctionnent très bien avec les irréductibles
CFG.

De mémoire, et probablement faux, SPEC2006 a une seule boucle irréductible dans
401.bzip2 et c'est tout. C'est assez rare en pratique.

Clang n'émettra qu'une seule instruction indirectbr dans les fonctions utilisant
goto calculé. Cela a pour effet de transformer les interprètes filetés en
boucles naturelles avec le bloc indirectbr comme en-tête de boucle. Après être parti
LLVM IR, le seul indirectbr est dupliqué en queue dans le générateur de code
pour reconstituer l'enchevêtrement d'origine.

-
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
.

Je peux aussi dire en outre que si des constructions irréductibles devaient être ajoutées à
WebAssembly, ils ne fonctionneraient pas dans TurboFan (JIT d'optimisation de V8), donc
les fonctions finiraient soit par être interprétées (extrêmement lentes) soit par être
compilé par un compilateur de base (un peu plus lent), car nous n'allons probablement pas
investir des efforts dans la mise à niveau de TurboFan pour prendre en charge un flux de contrôle irréductible.
Cela signifie que les fonctions avec un flux de contrôle irréductible dans WebAssembly seraient
finira probablement avec des performances bien pires.

Bien sûr, une autre option serait que le moteur WebAssembly en V8 exécute le
relooper pour alimenter les graphes réductibles TurboFan, mais cela ferait de la compilation
(et démarrage pire). Le rebouclage doit rester une procédure hors ligne dans mon
avis, sinon on se retrouve avec des coûts moteurs inéluctables.

Meilleur,
-Ben

Le 1er mai 2017 à 12h48, Ben L. Titzer [email protected] a écrit :

Il n'y a pas d'algorithme de vérification en un seul passage pour un contrôle irréductible
flux dont je suis conscient. Le choix de conception pour le flux de contrôle réductible uniquement
a été fortement influencée par cette exigence.

Comme mentionné précédemment, le flot de contrôle irréductible peut être modélisé au moins deux
différentes façons. Une boucle avec une instruction switch peut en fait être optimisée
dans le graphe irréductible d'origine par un simple saut de filetage local
optimisation (par exemple en pliant le motif où une affectation d'une constante
à une variable locale se produit, puis un branchement à un branchement conditionnel qui
active immédiatement cette variable locale).

Ainsi, les constructions de contrôle irréductibles ne sont pas du tout nécessaires, et il est
seulement une question d'une seule transformation backend du compilateur pour récupérer le
graphe irréductible original et l'optimiser (pour les moteurs dont les compilateurs
prend en charge un flux de contrôle irréductible - ce qu'aucun des 4 navigateurs ne fait, au
meilleur de ma connaissance).

Meilleur,
-Ben

Le jeu. 20 avril 2017 à 05:20, Jakob Stoklund Olesen <
[email protected]> a écrit :

Les optimisations de boucle dans LLVM ignoreront généralement le flux de contrôle irréductible
et ne pas essayer de l'optimiser. L'analyse de boucle sur laquelle ils sont basés sera
ne reconnaissent que les boucles naturelles, vous devez donc être conscient qu'il peut
être des cycles CFG qui ne sont pas reconnus comme des boucles. Bien sûr, d'autres
les optimisations sont de nature plus locale et fonctionnent très bien avec les irréductibles
CFG.

De mémoire, et probablement faux, SPEC2006 a une seule boucle irréductible
dans 401.bzip2 et c'est tout. C'est assez rare en pratique.

Clang n'émettra qu'une seule instruction indirectbr dans les fonctions utilisant
goto calculé. Cela a pour effet de transformer les interprètes filetés en
boucles naturelles avec le bloc indirectbr comme en-tête de boucle. Après être parti
LLVM IR, le seul indirectbr est dupliqué en queue dans le générateur de code
pour reconstituer l'enchevêtrement d'origine.

-
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
.

Il existe des méthodes établies pour la vérification en temps linéaire du flux de contrôle irréductible. Un exemple notable est la JVM : avec les stackmaps, elle a une vérification en temps linéaire. WebAssembly a déjà des signatures de bloc sur chaque construction de type bloc. Avec des informations de type explicites à chaque point où plusieurs chemins de flux de contrôle fusionnent, il n'est pas nécessaire d'utiliser des algorithmes à virgule fixe.

(En passant, il y a quelque temps, j'ai demandé pourquoi on interdirait à un opérateur hypothétique pick de lire en dehors de son bloc à des profondeurs arbitraires. Voici une réponse : à moins que les signatures ne soient étendues pour décrire tout un pick pourrait lire, la vérification du type de pick nécessiterait plus d'informations.)

Le modèle de boucle avec un commutateur peut bien sûr être supprimé, mais il n'est pas pratique de s'y fier. Si un moteur ne l'optimise pas, il aurait un niveau de surcharge perturbateur. Si la plupart des moteurs l'optimisent, alors on ne sait pas ce qui est accompli en gardant un flux de contrôle irréductible hors du langage lui-même.

Soupir… J'avais l'intention de répondre plus tôt mais la vie s'est mise en travers de mon chemin.

J'ai parcouru certains moteurs JS et je suppose que je dois affaiblir mon affirmation sur le flux de contrôle irréductible « fonctionnant ». Je ne pense toujours pas que ce serait si difficile de le faire fonctionner, mais il y a certaines constructions qui seraient difficiles à adapter d'une manière qui profiterait réellement à…

Eh bien, supposons, à des fins d'argumentation, que faire en sorte que le pipeline d'optimisation prenne en charge correctement un flux de contrôle irréductible est trop difficile. Un moteur JS peut toujours facilement le prendre en charge de manière piratée, comme ceci :

Dans le backend, traitez un bloc labels comme s'il s'agissait d'une boucle+commutateur jusqu'à la dernière minute. En d'autres termes, lorsque vous voyez un bloc labels , vous le traitez comme un en-tête de boucle avec un bord extérieur pointant vers chaque étiquette, et lorsque vous voyez un branch qui cible une étiquette, vous créez un bord pointant vers l'en- tête labels , pas l'étiquette cible réelle - qui devrait être stockée séparément quelque part. Pas besoin de créer une variable réelle pour stocker l'étiquette cible, comme le ferait une vraie boucle + commutateur ; il devrait suffire de cacher la valeur dans un champ de l'instruction de branchement ou de créer une instruction de contrôle distincte à cette fin. Ensuite, les optimisations, l'ordonnancement, voire l'allocation des registres peuvent tous prétendre qu'il y a deux sauts. Mais lorsque vient le temps de générer une instruction de saut native, vous vérifiez ce champ et générez un saut directement vers l'étiquette cible.

Il peut y avoir des problèmes avec, par exemple, toute optimisation qui fusionne/supprime des branches, mais il devrait être assez facile d'éviter cela ; les détails dépendent de la conception du moteur.

Dans un certain sens, ma suggestion est équivalente à la « simple optimisation locale de saut de filetage » de @titzer. Je suggère de faire en sorte que le flux de contrôle irréductible 'natif' ressemble à une boucle + commutateur, mais une alternative serait d'identifier de véritables boucles + commutateurs - c'est-à-dire le "modèle de @titzer où une affectation d'une constante à une variable locale se produit, puis une branche vers une branche conditionnelle qui active immédiatement cette variable locale » - et ajoutez des métadonnées permettant à la branche indirecte d'être supprimée tardivement dans le pipeline. Si cette optimisation devient omniprésente, elle pourrait être un substitut décent à une instruction explicite.

Quoi qu'il en soit, l'inconvénient évident de l'approche hacky est que les optimisations ne comprennent pas le graphe de flux de contrôle réel ; ils agissent effectivement comme si n'importe quelle étiquette pouvait sauter à n'importe quelle autre étiquette. En particulier, l'allocation de registre doit traiter une variable comme étant active dans toutes les étiquettes, même si, disons, elle est toujours assignée juste avant de sauter à une étiquette spécifique, comme dans ce pseudocode :

a:
  control = 1;
  goto x;
b:
  control = 2;
  goto x;
...
x:
  // use control

Cela pourrait conduire à une utilisation du registre sérieusement sous-optimale dans certains cas. Mais comme je le ferai remarquer plus tard, les algorithmes de vivacité que les JIT utilisent peuvent être fondamentalement incapables de bien le faire, de toute façon…

Quoi qu'il en soit, optimiser tardivement est bien mieux que ne pas optimiser du tout. Un seul saut direct est bien plus agréable qu'un saut + comparaison + charge + saut indirect ; le prédicteur de branche CPU peut éventuellement être capable de prédire la cible de ce dernier en fonction de l'état passé, mais pas aussi bien que le compilateur. Et vous pouvez éviter de dépenser un registre et/ou de la mémoire sur la variable « état actuel ».

Quant à la représentation, quelle est la meilleure : explicite ( labels instruction ou similaire) ou implicite (optimisation de la boucle réelle+commutateurs suivant un modèle spécifique) ?

Avantages implicites :

  • Maintient la spécification maigre.

  • Peut déjà fonctionner avec le code loop+switch existant. Mais je n'ai pas examiné les éléments générés par binaryen pour voir s'ils suivent un modèle suffisamment strict.

  • Faire en sorte que la manière bénie d'exprimer un flux de contrôle irréductible ressemble à un hack met en évidence le fait qu'il est plus lent en général et doit être évité dans la mesure du possible.

Inconvénients de l'implicite :

  • Cela ressemble à un hack. Certes, comme le dit @titzer , cela ne désavantage pas réellement les moteurs qui prennent « correctement » un flux de contrôle irréductible ; ils peuvent reconnaître le modèle de manière précoce et récupérer le flux irréductible d'origine avant d'effectuer des optimisations. Pourtant, il semble plus judicieux de simplement autoriser les vrais sauts.

  • Crée une « falaise d'optimisation », que WebAssembly est généralement censé éviter par rapport à JS. Pour recompter, le modèle de base à optimiser est « où se produit une affectation d'une constante à une variable locale, puis un branchement à un branchement conditionnel qui active immédiatement cette variable locale ». Mais que se passe-t-il si, disons, il y a d'autres instructions entre les deux, ou si l'affectation n'utilise pas réellement une instruction wasm const mais simplement quelque chose de constant en raison des optimisations ? Certains moteurs peuvent être plus libéraux que d'autres dans ce qu'ils reconnaissent comme ce modèle, mais le code qui en profite (intentionnellement ou non) aura des performances très différentes entre les navigateurs. Avoir un encodage plus explicite définit plus clairement les attentes.

  • Rend plus difficile l'utilisation de wasm comme un IR dans des étapes de post-traitement hypothétiques. Si un compilateur ciblant wasm fait les choses normalement et gère toutes les optimisations/transformations avec un IR interne avant d'exécuter éventuellement un relooper et finalement de générer wasm, alors cela ne dérangerait pas l'existence de séquences d'instructions magiques. Mais si un programme veut exécuter des transformations sur le code wasm lui-même, il devra éviter de briser ces séquences, ce qui serait ennuyeux.

Quoi qu'il en soit, cela m'est égal de toute façon - tant que, si nous décidons de l'approche implicite, les principaux navigateurs s'engagent réellement à effectuer l'optimisation appropriée.

Pour en revenir à la question de la prise en charge native des flux irréductibles - quels sont les obstacles, quels sont les avantages - voici quelques exemples spécifiques d'IonMonkey de passes d'optimisation qui devraient être modifiés pour le prendre en charge :

AliasAnalysis.cpp : itère sur les blocs en post-ordre inverse (une fois) et génère des dépendances de classement pour une instruction (telles qu'utilisées dans InstructionReordering) en ne regardant que les magasins précédemment vus comme un alias possible. Cela ne fonctionne pas pour le flux de contrôle cyclique. Mais les boucles (explicitement marquées) sont gérées spécialement, avec une deuxième passe qui vérifie les instructions dans les boucles par rapport à tout stockage ultérieur n'importe où dans la même boucle.

-> Il devrait donc y avoir un marquage de boucle pour les blocs labels . Dans ce cas, je pense que marquer l'ensemble du bloc labels comme une boucle "fonctionnerait" (sans marquer spécialement les étiquettes individuelles), car l'analyse est trop imprécise pour se soucier du flux de contrôle au sein de la boucle.

FlowAliasAnalysis.cpp : un algorithme alternatif un peu plus intelligent. Itère également sur les blocs dans l'ordre inverse, mais lorsqu'il rencontre chaque bloc, il fusionne les informations de dernier stockage calculées pour chacun de ses prédécesseurs (supposés avoir déjà été calculés), à l'exception des en-têtes de boucle, où il prend en compte le backedge.

-> Messier car il suppose que (a) les prédécesseurs des blocs de base individuels apparaissent toujours avant lui, à l'exception des backedges de boucle, et (b) une boucle ne peut avoir qu'un seul backedge. Il existe différentes manières de résoudre ce problème, mais cela nécessiterait probablement une gestion explicite de labels , et pour que l'algorithme reste linéaire, il devrait probablement fonctionner assez grossièrement dans ce cas, plus comme un AliasAnalysis normal

BacktrackingAllocator.cpp : comportement similaire pour l'allocation de registres : il effectue un passage inverse linéaire à travers la liste d'instructions et suppose que toutes les utilisations d'une instruction apparaîtront après (c'est-à-dire seront traitées avant) sa définition, sauf lors de la rencontre de backedges de boucle : registres qui sont en direct au début d'une boucle, restez simplement en direct pendant toute la boucle.

-> Chaque étiquette devrait être traitée comme un en-tête de boucle, mais la vivacité devrait s'étendre à l'ensemble du bloc d'étiquettes. Pas difficile à mettre en œuvre, mais encore une fois, le résultat ne serait pas meilleur que l'approche hacky. Je pense.

@comex Une autre considération ici est de savoir combien les moteurs wasm sont censés faire. Par exemple, vous mentionnez AliasAnalysis d'Ion ci-dessus, mais l'autre côté de l'histoire est que l'analyse des alias n'est pas si importante pour le code WebAssembly, du moins pour l'instant alors que la plupart des programmes utilisent la mémoire linéaire.

L'algorithme de vivacité BacktrackingAllocator.cpp d'Ion nécessiterait un certain travail, mais il ne serait pas prohibitif. La plupart d'Ion gère déjà diverses formes de flux de contrôle irréductibles, car l'OSR peut créer plusieurs entrées dans des boucles.

Une question plus large ici est de savoir quelles optimisations les moteurs WebAssembly devront faire. Si l'on s'attend à ce que WebAssembly soit une plate-forme de type assemblage, avec des performances prévisibles où les producteurs/bibliothèques effectuent la majeure partie de l'optimisation, alors un flux de contrôle irréductible serait un coût assez faible car les moteurs n'auraient pas besoin des grands algorithmes complexes où c'est un fardeau important . Si l'on s'attend à ce que WebAssembly soit un bytecode de niveau supérieur, qui effectue automatiquement plus d'optimisation de haut niveau, et que les moteurs soient plus complexes, il devient alors plus utile de garder un flux de contrôle irréductible hors du langage, pour éviter la complexité supplémentaire.

BTW, il convient également de mentionner dans ce numéro l' algorithme de construction SSA à la volée de Braun et al , qui est un

Je suis intéressé par l'utilisation de WebAssembly en tant que backend qemu sur iOS, où WebKit (et l'éditeur de liens dynamique, mais qui vérifie la signature du code) est le seul programme autorisé à marquer la mémoire comme exécutable. Le codegen de Qemu suppose que les instructions goto feront partie de tout processeur pour lequel il doit coder, ce qui rend un backend WebAssembly presque impossible sans l'ajout de gotos.

@tbodt - Pourriez-vous utiliser le relooper de Binaryen ? Cela vous permet de générer ce qui est essentiellement Wasm-with-goto, puis de le convertir en flux de contrôle structuré pour Wasm.

@eholk Cela semble être beaucoup plus lent qu'une traduction directe du code machine en wasm.

@tbodt L' utilisation de Binaryen ajoute un IR supplémentaire en cours de route, oui, mais cela ne devrait pas être beaucoup plus lent, je pense, il est optimisé pour la vitesse de compilation. Et cela peut également avoir des avantages autres que la gestion des gotos, etc., car vous pouvez éventuellement exécuter l'optimiseur Binaryen, qui peut faire des choses que l'optimiseur qemu ne fait pas (choses spécifiques à wasm).

En fait, je serais très intéressé de collaborer avec vous là-dessus, si vous le souhaitez :) Je pense que le portage de Qemu sur wasm serait très utile.

Donc, à la réflexion, les gotos n'aideraient pas vraiment beaucoup. Le codegen de Qemu génère le code des blocs de base lors de leur première exécution. Si un bloc passe à un bloc qui n'a pas encore été généré, il génère le bloc et corrige le bloc précédent avec un goto au bloc suivant. Pour autant que je sache, le chargement dynamique de code et la correction des fonctions existantes ne sont pas des choses qui peuvent être faites en webassembly.

@kripken Je serais intéressé à collaborer, quel serait le meilleur endroit pour discuter avec vous ?

Vous ne pouvez pas patcher les fonctions existantes directement, mais vous pouvez utiliser call_indirect et le a WebAssembly.Table pour jit code. Pour tout bloc de base qui n'a pas été généré, vous pouvez appeler JavaScript, générer le module WebAssembly et l'instance de manière synchrone, extraire la fonction exportée et l'écrire sur l'index dans la table. Les futurs appels utiliseront alors votre fonction générée.

Je ne suis pas sûr que quelqu'un ait déjà essayé cela, cependant, il y aura probablement de nombreux aspérités.

Cela pourrait fonctionner si les appels de queue étaient implémentés. Sinon, la pile déborderait assez rapidement.

Un autre défi serait d'allouer de l'espace dans la table par défaut. Comment mapper une adresse à un index de table ?

Une autre option consiste à régénérer la fonction wasm sur chaque nouveau bloc de base. Cela signifie un nombre de recompilations égal au nombre de blocs utilisés, mais je parie que c'est le seul moyen de faire fonctionner le code rapidement après sa compilation (en particulier les boucles internes), et il n'a pas besoin d'être complet recompiler, nous pouvons réutiliser l'IR Binaryen pour chaque bloc existant, ajouter l'IR pour le nouveau bloc et exécuter simplement le relooper sur chacun d'eux.

(Mais peut-être pouvons-nous demander à qemu de compiler toute la fonction à l'avance au lieu de paresseusement ?)

@tbodt pour collaborer avec Binaryen, une option consiste à créer un dépôt avec votre travail (et peut y utiliser des problèmes, etc.), une autre consiste à ouvrir un problème spécifique dans Binaryen pour qemu.

Nous ne pouvons pas faire en sorte que qemu compile une fonction entière à la fois, car qemu n'a pas de concept de "fonction".

Quant à la recompilation de l'ensemble du cache de blocs, cela peut prendre beaucoup de temps. Je vais découvrir comment utiliser le profileur intégré de qemu, puis ouvrir un problème sur binaryen.

Question secondaire. À mon avis, un langage ciblant WebAssembly devrait être capable de fournir une fonction mutuellement récursive efficace. Pour une description de leur utilité, je vous invite à lire : http://sharp-gamedev.blogspot.com/2011/08/forgotten-control-flow-construct.html

En particulier, le besoin exprimé par Cheery semble être adressé par une fonction mutuellement récursive.

Je comprends la nécessité de la récursivité de la queue, mais je me demande si la fonction mutuellement récursive ne peut être implémentée que si la machinerie sous-jacente fournit des gotos ou non. S'ils le font, cela constitue pour moi un argument légitime en leur faveur, car il y aura une tonne de langages de programmation qui auront du mal à cibler WebAssembly autrement. S'ils ne le font pas, alors peut-être que le mécanisme minimum pour prendre en charge la fonction mutuellement récursive est tout ce qui serait nécessaire (avec la récursivité de la queue).

@davidgrenier , les fonctions d'un module Wasm sont toutes mutuellement récursives. Pouvez-vous préciser ce que vous considérez comme inefficace à leur sujet ? Faites-vous seulement référence au manque d'appels de queue ou à autre chose ?

Les appels de queue généraux arrivent. La récursivité de la queue (mutuelle ou autre) en sera un cas particulier.

Je ne disais pas que quelque chose était inefficace à leur sujet. Je dis que si vous les avez, vous n'avez pas besoin de goto général car les fonctions mutuellement récursives fournissent tout ce dont l'implémenteur de langage ciblant WebAssembly devrait avoir besoin.

Goto est très utile pour la génération de code à partir de diagrammes en programmation visuelle. Peut-être que maintenant la programmation visuelle n'est pas très populaire, mais à l'avenir, elle pourra attirer plus de monde et je pense que wasm devrait être prêt pour cela. En savoir plus sur la génération de code à partir des diagrammes et aller à : http://drakon-editor.sourceforge.net/generation.html

La prochaine version Go 1.11 aura un support expérimental pour WebAssembly. Cela inclura une prise en charge complète de toutes les fonctionnalités de Go, y compris les goroutines, les canaux, etc. Cependant, les performances du WebAssembly généré ne sont actuellement pas très bonnes.

Ceci est principalement dû à l'instruction goto manquante. Sans l'instruction goto, nous avons dû recourir à une boucle de niveau supérieur et à une table de saut dans chaque fonction. L'utilisation de l'algorithme relooper n'est pas une option pour nous, car lors du basculement entre les goroutines, nous devons pouvoir reprendre l'exécution à différents points d'une fonction. Le relooper ne peut pas aider avec cela, seule une instruction goto peut le faire.

C'est génial que WebAssembly en soit arrivé au point où il peut prendre en charge un langage comme Go. Mais pour être vraiment l'assembleur du Web, WebAssembly doit être aussi puissant que les autres langages d'assemblage. Go dispose d'un compilateur avancé capable d'émettre un assemblage très efficace pour un certain nombre d'autres plates-formes. C'est pourquoi je voudrais argumenter que c'est principalement une limitation de WebAssembly et non du compilateur Go qu'il n'est pas possible d'utiliser également ce compilateur pour émettre des assemblys efficaces pour le web.

L'utilisation de l'algorithme relooper n'est pas une option pour nous, car lors du basculement entre les goroutines, nous devons pouvoir reprendre l'exécution à différents points d'une fonction.

Juste pour clarifier, un goto régulier ne suffirait pas pour cela, un goto calculé est requis pour votre cas d'utilisation, n'est-ce pas ?

Je pense qu'un goto régulier serait probablement suffisant en termes de performances. Les sauts entre les blocs de base sont de toute façon statiques et pour changer de goroutine, un br_table avec des gotos dans ses branches devrait être assez performant. La taille de sortie est une question différente cependant.

Il semble que vous ayez un flux de contrôle normal dans chaque fonction, mais que vous ayez également besoin de la possibilité de passer de l'entrée de la fonction à certains autres emplacements au "milieu", lors de la reprise d'une goroutine - combien y a-t-il de tels emplacements ? S'il s'agit de chaque bloc de base, le relooper serait obligé d'émettre une boucle de niveau supérieur par laquelle chaque instruction passe, mais s'il ne s'agit que de quelques-uns, cela ne devrait pas être un problème. (C'est en fait ce qui se passe avec la prise en charge de setjmp dans emscripten - nous créons simplement les chemins supplémentaires nécessaires entre les blocs de base de LLVM et laissons le relooper le traiter normalement.)

Chaque appel à une autre fonction est un tel emplacement et la plupart des blocs de base ont au moins une instruction d'appel. Nous sommes plus ou moins en train de dérouler et de restaurer la pile d'appels.

Je vois, merci. Oui, je suis d'accord que pour que cela soit pratique, vous avez besoin d'un support de restauration de goto statique ou de pile d'appels (ce qui a également été pris en compte).

Sera-t-il possible d'appeler une fonction dans le style CPS ou d'implémenter call/cc dans WASM ?

@Heimdell , la prise en charge d'une certaine forme de continuations délimitées (alias "commutation de pile") est sur la feuille de route, ce qui devrait suffire pour presque toutes les abstractions de contrôle intéressantes. Nous ne pouvons cependant pas prendre en charge les continuations illimitées (c'est-à-dire appel/cc complet), car la pile d'appels Wasm peut être arbitrairement mélangée avec d'autres langages, y compris les appels réentrants vers l'embedder, et ne peut donc pas être supposée être copiable ou déplaçable.

En lisant ce fil, j'ai l'impression que les étiquettes et les gotos arbitraires ont un obstacle majeur avant de devenir une fonctionnalité :

  • Le flux de contrôle non structuré permet des graphiques de flux de contrôle irréductibles
  • Élimination* de toute « vérification simple et rapide, conversion facile en un seul passage vers le formulaire SSA »
  • Ouverture du compilateur JIT aux performances non linéaires
  • Les personnes naviguant sur des pages Web ne devraient pas subir de retards si le compilateur de la langue d'origine peut effectuer le travail initial

_*bien qu'il puisse exister des alternatives telles que l' algorithme de construction SSA à la volée de

Si nous sommes toujours bloqués là-bas, les appels _and_ tail avancent, peut-être qu'il vaudrait la peine de demander aux compilateurs de langage de continuer à traduire en gotos, mais comme dernière étape avant la sortie de WebAssembly, divisez les "blocs d'étiquettes" en fonctions, et convertir les gotos en appels de queue.

Selon l'article de 1977 du concepteur de schéma Guy Steele, Lambda: The Ultimate GOTO , la transformation devrait être possible et les performances des appels de queue devraient pouvoir correspondre étroitement aux gotos.

Les pensées?

Si nous sommes toujours bloqués là-bas, les appels _and_ tail avancent, peut-être qu'il vaudrait la peine de demander aux compilateurs de langage de continuer à traduire en gotos, mais comme dernière étape avant la sortie de WebAssembly, divisez les "blocs d'étiquettes" en fonctions, et convertir les gotos en appels de queue.

C'est essentiellement ce que tout compilateur ferait de toute façon, personne à ma connaissance ne préconise des gotos non gérés du genre qui causent tant de problèmes dans la JVM, juste pour un graphique d'EBB typés. LLVM, GCC, Cranelift et les autres ont tous un CFG de forme SSA (éventuellement irréductible) comme représentation interne et les compilateurs de Wasm à natif ont la même représentation interne, nous voulons donc conserver autant de ces informations que possible et reconstruire le moins possible de ces informations. Les locaux sont avec pertes, car ils ne sont plus SSA, et le flux de contrôle de Wasm est avec pertes, car ce n'est plus un CFG arbitraire. AFAIK, faire en sorte que Wasm soit une machine de registre SSA à registre infini avec des informations de vivacité de registre à grain fin intégrées serait probablement le meilleur pour le codegen mais la taille du code gonflerait, une machine à pile avec un flux de contrôle modélisé sur un CFG arbitraire est probablement le meilleur compromis . Je me trompe peut-être sur la taille du code avec une machine à registrer, il pourrait être possible de l'encoder efficacement.

Le problème avec le flux de contrôle irréductible est que s'il est irréductible sur le front-end, il est toujours irréductible dans wasm, la conversion relooper/stackifier ne rend pas le flux de contrôle réductible, il convertit simplement l'irréductibilité pour qu'elle dépende des valeurs d'exécution. Cela donne moins d'informations au backend et peut donc produire un code pire, la seule façon de produire un bon code pour les CFG irréductibles en ce moment est de détecter les modèles émis par relooper et stackifier et de les reconvertir en un CFG irréductible. À moins que vous ne développiez V8, qui, selon AFAIK, ne prend en charge que le flux de contrôle réductible, la prise en charge du flux de contrôle irréductible est purement une victoire - cela simplifie les frontends et les backends (les frontends peuvent simplement émettre du code dans le même format qu'ils le stockent en interne, les backends ne pas besoin de détecter des modèles) tout en produisant une meilleure sortie dans le cas où le flux de contrôle est irréductible et une sortie qui est tout aussi bonne ou meilleure dans le cas habituel où le flux de contrôle est réductible.

De plus, cela permettrait à GCC et Go de commencer à produire WebAssembly.

Je sais que V8 est un composant important de l'écosystème WebAssembly, mais il semble que ce soit la seule partie de cet écosystème qui bénéficie de la situation actuelle du flux de contrôle, tous les autres backends que je connais se convertissent de toute façon en CFG et ne sont pas affectés par si WebAssembly peut représenter un flux de contrôle irréductible ou non.

La v8 ne pourrait-elle pas simplement incorporer le relooper afin d'accepter les CFG d'entrée ? Il semble que de gros morceaux de l'écosystème soient bloqués sur les détails de mise en œuvre de la v8.

Juste pour référence, j'ai remarqué que les instructions switch en c++ sont très lentes dans wasm. Lorsque j'ai profilé le code, j'ai dû les convertir en d'autres formes qui fonctionnaient beaucoup plus rapidement pour faire du traitement d'image. Et ce n'était jamais un problème sur les autres architectures. J'aimerais vraiment goto pour des raisons de performances.

@graph , pouvez-vous fournir plus de détails sur la lenteur des instructions de commutation ? Toujours à la recherche d'une opportunité d'améliorer les performances... (Si vous ne voulez pas vous enliser dans ce fil, envoyez-moi un e-mail directement, [email protected].)

Je posterai ici car cela s'applique à tous les navigateurs. Des instructions simples comme celle-ci lorsqu'elles sont compilées avec emscripten étaient plus rapides lorsque je les ai converties en instructions if.

for(y = ....) {
    for(x = ....) {
        switch(type){
        case IS_RGBA:....
         ....
        case IS_BGRA
        ....
        case IS_RGB
        ....
....

Je suppose que le compilateur convertissait une table de saut en tout ce que wasm prend en charge. Je n'ai pas regardé dans l'assemblage généré, donc je ne peux pas confirmer.

Je connais quelques choses sans rapport avec wasm qui peuvent être optimisées pour le traitement d'images sur le Web. Je l'ai déjà soumis via le bouton "feedback" dans firefox. Si vous êtes intéressé, faites le moi savoir et je vous enverrai les problèmes par e-mail.

@graph Un benchmark complet serait très utile ici. En général, un commutateur en C peut se transformer en une table de saut très rapide dans wasm, mais il y a des cas particuliers qui ne fonctionnent pas encore bien, que nous devrons peut-être corriger, que ce soit dans LLVM ou dans les navigateurs.

Dans emscripten en particulier, la façon dont les commutateurs sont gérés change beaucoup entre l'ancien backend fastcomp et le nouveau backend en amont, donc si vous l'avez vu il y a quelque temps, ou récemment mais en utilisant fastcomp, il serait bon de vérifier en amont.

@graph , Si emscripten produit un br_table alors le jit générera parfois une table de saut et parfois (s'il pense que ce sera plus rapide) il recherchera l'espace clé de manière linéaire ou avec une recherche binaire en ligne. Ce qu'il fait dépend souvent de la taille du commutateur. Il est bien sûr possible que la politique de sélection ne soit pas optimale... Je suis d'accord avec @kripken , du code

(Je ne sais pas pour v8 ou jsc, mais Firefox ne reconnaît actuellement pas une chaîne if-then-else comme un commutateur possible, donc ce n'est généralement pas une bonne idée d'ouvrir des commutateurs de code aussi longs que des chaînes if-then-else. Le le seuil de rentabilité n'est probablement pas à plus de deux ou trois comparaisons.)

@lars-t-hansen @kripken @graph il se pourrait bien que br_table soit actuellement très peu optimisé comme cet échange semble le montrer : https://twitter.com/battagline/status/1168310096515883008

@aardappel , c'est curieux, les benchmarks que j'ai

S'il ne peut pas faire l'analyse de la plage sur la valeur du commutateur pour l'éviter, la br_table devra également effectuer au moins un test de filtrage pour la plage du commutateur, ce qui nuit également à son avantage.

@lars-t-hansen Oui, nous ne connaissons pas son cas de test, peut-être qu'il avait une valeur aberrante. Quoi qu'il en soit, il semble que Chrome ait plus de travail à faire que Firefox.

Je suis en vacances, d'où mon manque de réponses. Merci de votre compréhension.

@kripken @lars-t-hansen J'ai effectué quelques tests, il semble que oui, c'est mieux maintenant dans firefox. Il existe encore des cas où if-else surpasse le commutateur. Voici un cas :


Main.cpp

#include <stdio.h>

#include <chrono>
#include <random>

class Chronometer {
public:
    Chronometer() {

    }

    void start() {
        mStart = std::chrono::steady_clock::now();
    }

    double seconds() {
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        return std::chrono::duration_cast<std::chrono::duration<double>>(end - mStart).count();
    }

private:
    std::chrono::steady_clock::time_point mStart;
};

int main() {
    printf("Starting tests!\n");
    Chronometer timer;
    // we want to prevent optimizations based on known size as most applications
    // do not know the size in advance.
    std::random_device rd;  //Will be used to obtain a seed for the random number engine
    std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
    std::uniform_int_distribution<> dis(100000000, 1000000000);
    std::uniform_int_distribution<> opKind(0, 3);
    int maxFrames = dis(gen);
    int switchSelect = 0;
    constexpr int SW1 = 1;
    constexpr int SW2 = 8;
    constexpr int SW3 = 32;
    constexpr int SW4 = 38;

    switch(opKind(gen)) {
    case 0:
        switchSelect = SW1;
        break;
    case 1:
        switchSelect = SW2; break;
    case 2:
        switchSelect = SW3; break;
    case 4:
        switchSelect = SW4; break;
    }
    printf("timing with SW = %d\n", switchSelect);
    timer.start();
    int accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        switch(switchSelect) {
        case SW1:
            accumulator = accumulator*3 + i; break;
        case SW2:
            accumulator = (accumulator < 3)*i; break;
        case SW3:
            accumulator = (accumulator&0xFF)*i + accumulator; break;
        case SW4:
            accumulator = (accumulator*accumulator) - accumulator + i; break;
        }
    }
    printf("switch time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);
    timer.start();
    accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        if(switchSelect == SW1)
            accumulator = accumulator*3 + i;
        else if(switchSelect == SW2)
            accumulator = (accumulator < 3)*i;
        else if(switchSelect == SW3)
            accumulator = (accumulator&0xFF)*i + accumulator;
        else if(switchSelect == SW4)
            accumulator = (accumulator*accumulator) - accumulator + i;
    }
    printf("if-else time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);

    return 0;
}

Selon la valeur de switchSelect. if-else surpasse. Exemple de sortie :

Starting tests!
timing with SW = 32
switch time = 2.049000 seconds
accumulated value: 0
if-else time = 0.401000 seconds
accumulated value: 0

Comme vous pouvez le voir pour switchSelect = 32 if-else est beaucoup plus rapide. Pour les autres cas, if-else est un peu plus rapide. Pour le cas switchSelect = 1 & 0, l'instruction switch est plus rapide.

Test in Firefox 69.0.3 (64-bit)
compiled using: emcc -O3 -std=c++17 main.cpp -o main.html
emcc version: emcc (Emscripten gcc/clang-like replacement) 1.39.0 (commit e047fe4c1ecfae6ba471ca43f2f630b79516706b)

Utilisation du dernier emscripen stable en date du 20 octobre 2019. Nouvelle installation ./emcc activate latest .

J'ai remarqué ci-dessus qu'il y a une faute de frappe, mais cela ne devrait pas affecter le fait que le if-else est le cas SW3 plus rapide car ils exécutent les mêmes instructions.

encore une fois avec cela au-delà du seuil de rentabilité de 5: Intéressant que pour switchSelect = 32 pour ce cas, sa vitesse est similaire à if-else. Comme vous pouvez le voir pour 1003 if-else est légèrement plus rapide. Switch devrait gagner dans ce cas.

Starting tests!
timing with SW = 1003
switch time = 2.253000 seconds
accumulated value: 1903939380
if-else time = 2.197000 seconds
accumulated value: 1903939380


main.cpp

#include <stdio.h>

#include <chrono>
#include <random>

class Chronometer {
public:
    Chronometer() {

    }

    void start() {
        mStart = std::chrono::steady_clock::now();
    }

    double seconds() {
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        return std::chrono::duration_cast<std::chrono::duration<double>>(end - mStart).count();
    }

private:
    std::chrono::steady_clock::time_point mStart;
};

int main() {
    printf("Starting tests!\n");
    Chronometer timer;
    // we want to prevent optimizations based on known size as most applications
    // do not know the size in advance.
    std::random_device rd;  //Will be used to obtain a seed for the random number engine
    std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
    std::uniform_int_distribution<> dis(100000000, 1000000000);
    std::uniform_int_distribution<> opKind(0, 8);
    int maxFrames = dis(gen);
    int switchSelect = 0;
    constexpr int SW1 = 1;
    constexpr int SW2 = 8;
    constexpr int SW3 = 32;
    constexpr int SW4 = 38;
    constexpr int SW5 = 64;
    constexpr int SW6 = 67;
    constexpr int SW7 = 1003;
    constexpr int SW8 = 256;

    switch(opKind(gen)) {
    case 0:
        switchSelect = SW1;
        break;
    case 1:
        switchSelect = SW2; break;
    case 2:
        switchSelect = SW3; break;
    case 3:
        switchSelect = SW4; break;
    case 4:
        switchSelect = SW5; break;
    case 5:
        switchSelect = SW6; break;
    case 6:
        switchSelect = SW7; break;
    case 7:
        switchSelect = SW8; break;
    }
    printf("timing with SW = %d\n", switchSelect);
    timer.start();
    int accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        switch(switchSelect) {
        case SW1:
            accumulator = accumulator*3 + i; break;
        case SW2:
            accumulator = (accumulator < 3)*i; break;
        case SW3:
            accumulator = (accumulator&0xFF)*i + accumulator; break;
        case SW4:
            accumulator = (accumulator*accumulator) - accumulator + i; break;
        case SW5:
            accumulator = (accumulator << 3) - accumulator + i; break;
        case SW6:
            accumulator = (i - accumulator) & 0xFF; break;
        case SW7:
            accumulator = i*i + accumulator; break;
        }
    }
    printf("switch time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);
    timer.start();
    accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        if(switchSelect == SW1)
            accumulator = accumulator*3 + i;
        else if(switchSelect == SW2)
            accumulator = (accumulator < 3)*i;
        else if(switchSelect == SW3)
            accumulator = (accumulator&0xFF)*i + accumulator;
        else if(switchSelect == SW4)
            accumulator = (accumulator*accumulator) - accumulator + i;
        else if(switchSelect == SW5)
            accumulator = (accumulator << 3) - accumulator + i;
        else if(switchSelect == SW6)
            accumulator = (i - accumulator) & 0xFF;
        else if(switchSelect == SW7)
            accumulator = i*i + accumulator;

    }
    printf("if-else time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);

    return 0;
}


Merci les gars d'avoir jeté un coup d'œil à ces cas de test.

C'est un switch très clairsemé, que LLVM devrait de toute façon convertir en l'équivalent d'un ensemble de if-then, mais apparemment, il le fait d'une manière moins efficace que le manuel if-then. Avez-vous essayé d'exécuter wasm2wat pour voir en quoi ces deux boucles diffèrent dans le code ?

Cela dépend aussi fortement de ce test utilisant la même valeur à chaque itération. Ce test serait meilleur s'il passait en revue toutes les valeurs, ou mieux encore, choisi au hasard parmi elles (si cela peut être fait à moindre coût).

Mieux encore, la vraie raison pour laquelle les gens utilisent le commutateur pour les performances est avec une plage dense, vous pouvez donc garantir qu'il utilise réellement br_table dessous. Voir combien de cas br_table est plus rapide que if serait la chose la plus utile à savoir.

Le commutateur dans les boucles serrées a été utilisé car il s'agissait d'un code plus propre par rapport aux performances. Mais pour wasm, l'impact sur les performances était trop important, il a donc été converti en instructions if plus laides. Pour le traitement d'image dans beaucoup de mes cas d'utilisation, si je veux plus de performances d'un commutateur, je déplacerais le commutateur en dehors de la boucle et j'aurais simplement des copies de la boucle pour chaque cas. Habituellement, le commutateur bascule simplement entre une forme de format de pixel, un format de couleur, un codage, etc. Et dans de nombreux cas, les constantes sont calculées via des définitions ou des énumérations et non linéaires. Je vois maintenant que mon problème n'est pas lié à goto design. J'avais juste une compréhension incomplète de ce qui se passait pour mes déclarations de commutation. J'espère que mes notes seront utiles aux développeurs de navigateurs qui lisent ceci pour optimiser wasm pour le traitement d'images dans ces cas. Merci.

Je n'aurais jamais pensé que goto puisse être un débat aussi houleux 😮 . Je suis dans le bateau de chaque langue devrait avoir un goto 😁 . Une autre raison d'ajouter goto est que cela réduit la complexité pour le compilateur de compiler en wasm. Je suis presque sûr que c'est mentionné ci-dessus quelque part. Maintenant, je n'ai rien à redire 😞 .

Y a-t-il d'autres progrès là-bas?

en raison du débat houleux, je suppose qu'un navigateur ajouterait la prise en charge de goto en tant qu'extension de bytecode non standard. Alors peut-être que GCC peut entrer dans le jeu en prenant en charge une version non standard. Ce qui, à mon avis, n'est pas bon dans l'ensemble, mais permettra une plus grande concurrence entre les compilateurs. Cela a-t-il été pris en compte ?

Il n'y a pas eu beaucoup de progrès ces derniers temps, mais vous voudrez peut-être regarder la proposition de funclets .

@graph pour moi, votre suggestion ressemble à "
Ça ne marche pas comme ça. Il y a BEAUCOUP d'avantages de la structure actuelle de WebAssembly (qui ne sont malheureusement pas évidents). Essayez de plonger plus profondément dans la philosophie du wasm.

Autoriser les « étiquettes et Gotos arbitraires » nous ramènera à l'époque (antique) du bytecode non vérifiable. Tous les compilateurs passeront simplement à une "façon paresseuse" de faire les choses, au lieu de "le faire correctement".

Il est clair que le wasm dans son état actuel comporte des omissions majeures. Les gens travaillent à combler les lacunes (comme celle mentionnée par @binji ), mais je ne pense

@vshymanskyy La proposition de funclets, qui fournit des fonctionnalités équivalentes à des étiquettes et des gotos arbitraires, est entièrement validable, en temps linéaire.

Je dois également mentionner que dans notre compilateur Wasm en temps linéaire, nous compilons en interne tout le flux de contrôle Wasm dans une représentation de type funclets, sur laquelle j'ai quelques informations dans ce bloc de message et la conversion du flux de contrôle Wasm en cette représentation interne est implémentée ici . Le compilateur obtient toutes ses informations de type à partir de cette représentation de type funclets, il suffit donc de dire qu'il est trivial de valider la sécurité de type de celui-ci en temps linéaire.

Je pense que cette idée fausse selon laquelle le flux de contrôle irréductible ne peut pas être validé en temps linéaire provient de la JVM, où le flux de contrôle irréductible doit être exécuté à l'aide de l'interpréteur au lieu d'être compilé. C'est parce que la JVM n'a aucun moyen de représenter les métadonnées de type pour un flux de contrôle irréductible, et donc elle ne peut pas faire la conversion de machine de pile en machine de registre. Les "gotos arbitraires" (c'est-à-dire sauter à l'octet/à l'instruction X) n'est pas du tout vérifiable, mais séparer une fonction en blocs typés, qui peuvent ensuite être sautés dans un ordre arbitraire, n'est pas plus difficile à vérifier que de séparer un module en fonctions typées , qui peut ensuite être sauté entre dans un ordre arbitraire. Vous n'avez pas besoin de gotos non typés de style jump-to-byte-X pour implémenter des modèles utiles qui seraient émis par des compilateurs comme GCC et LLVM.

J'adore le processus ici. La face A explique pourquoi cela est nécessaire dans des applications spécifiques. La face B dit qu'ils se trompent, mais n'offre aucun support pour cette application. La face A explique comment aucun des arguments pragmatiques de B ne tient la route. Le côté B ne veut pas s'en occuper parce qu'il pense que le côté A le fait mal. La face A essaie d'atteindre un objectif. La face B dit que ce n'est pas le bon objectif, le qualifiant de paresseux ou de brutal. Les significations philosophiques plus profondes sont perdues du côté A. Le pragmatique est perdu du côté B, car ils prétendent avoir une sorte de fondement moral plus élevé. La face A considère cela comme une opération mécaniste amorale. En fin de compte, la face B garde généralement le contrôle de la spécification pour le meilleur ou pour le pire, et elle a fait un travail incroyable avec sa pureté relative.

Honnêtement, je viens de mettre mon nez ici parce qu'il y a des années, j'essayais de faire un port TinyCC vers WASM afin que je puisse exécuter un environnement de développement sur un ESP8266 ciblant l'ESP8266. Je n'ai qu'environ 4 Mo de stockage, il est donc hors de question d'inclure le re-looper et le passage à un AST, ainsi que de nombreux autres changements. (Remarque : en quoi relooper est-il la seule chose comme relooper ? C'est tellement horrible et personne n'a réécrit ce meunier en C ! ?) Même si c'était possible à ce stade, je ne sais pas si j'écrirais une cible TinyCC à WASM car ce n'est plus aussi intéressant pour moi.

Ce fil, cependant. Sainte vache ce fil m'a apporté tellement de joie existentielle. Regarder une bifurcation dans l'humanité est plus profonde que démocrate ou républicain, ou religion. J'ai l'impression que cela peut jamais être résolu. Si A peut venir vivre dans le monde de B, ou si B valide l'affirmation de A selon laquelle la programmation procédurale a sa place... J'ai l'impression que nous pourrions résoudre la paix dans le monde.

Quelqu'un en charge de V8 pourrait-il confirmer dans ce fil que l'opposition contre le flux de contrôle irréductible n'est pas influencée par la mise en œuvre actuelle de V8 ?

Je demande parce que c'est ce qui me dérange le plus. Il me semble que cela devrait être une discussion au niveau des spécifications sur les avantages et les inconvénients de cette fonctionnalité. Il ne devrait pas du tout être influencé par la façon dont une mise en œuvre particulière est actuellement conçue. Cependant, il y a eu des déclarations qui me font croire que la mise en œuvre de V8 influence cela. Peut-être que je me trompe. Une déclaration ouverte pourrait aider.

Eh bien, même si c'est malheureux, les implémentations actuelles existantes jusqu'à présent sont si importantes que l'avenir (probablement plus long que le passé) n'est pas si important. J'essayais d'expliquer qu'au #1202, que la cohérence est plus importante que les quelques implémentations, mais il semble que je me fasse des illusions. Bonne chance d'expliquer que certaines décisions de développement quelque part dans un projet ne constituent pas une vérité universelle, ou doivent être, par défaut, supposées correctes.

Ce fil est un canari dans la mine de charbon W3C. Bien que j'aie un grand respect pour de nombreuses personnes du W3C, la décision de confier JavaScript à Ecma International, et non au W3C, n'a pas été prise sans préjudice.

Comme @cnlohr ,

« Wasm est conçu comme une cible de compilation portable pour les langages de programmation, permettant le déploiement sur le Web pour les applications client et serveur. - webassembly.org

Bien sûr, n'importe qui peut pontifier pourquoi goto est [INSÉRER LE JARGON], mais que diriez-vous de préférer les normes aux opinions. Nous pouvons tous convenir que POSIX C est une bonne cible de référence, d'autant plus que les langages d'aujourd'hui sont soit élaborés à partir de C, soit comparés à ceux-ci et que le titre de la page d'accueil de WASM se vante d'être une cible de compilation portable pour les langages. Bien sûr, certaines fonctionnalités seront planifiées comme les threads et simd. Mais, ignorer totalement quelque chose d'aussi fondamental que goto , ne même pas lui donner la décence d'une feuille de route, n'est pas conforme à l'objectif déclaré du WASM et à une telle position de l'organisme de normalisation qui a donné le feu vert à <marquee> est au-delà de la pâleur.

Selon la norme de codage SEI CERT C Rec. « Envisagez d'utiliser une chaîne goto lorsque vous laissez une fonction en erreur lors de l'utilisation et de la libération de ressources » ;

De nombreuses fonctions nécessitent l'allocation de plusieurs ressources. Un échec et un retour quelque part au milieu de cette fonction sans libérer toutes les ressources allouées pourraient produire une fuite de mémoire. C'est une erreur courante d'oublier de libérer une (ou toutes) des ressources de cette manière, donc une chaîne goto est le moyen le plus simple et le plus propre d'organiser les sorties tout en préservant l'ordre des ressources libérées.

La recommandation propose ensuite un exemple avec la solution POSIX C préférée utilisant goto . Les opposants souligneront que goto est toujours considéré comme nuisible . Fait intéressant, cette opinion n'est pas incarnée dans l'une de ces normes de codage particulières, juste une note. Ce qui nous amène au canari, le "considéré comme nuisible".

En fin de compte, considérer les « régions CSS » ou goto comme nuisibles ne devrait être pesé qu'avec une solution proposée au problème pour lequel une telle fonctionnalité est utilisée. Si supprimer ladite fonctionnalité "nuisible" revient à supprimer les cas d'utilisation raisonnables sans alternative, ce n'est pas une solution, c'est en fait dommageable pour les utilisateurs du langage.

Les fonctions ne sont pas à coût nul, même en C. Si quelqu'un propose un remplacement aux gotos & labels, canihaz s'il vous plaît ! Si quelqu'un dit que je n'en ai pas besoin, comment le sait-il ? En ce qui concerne les performances, goto peut nous donner ce petit plus, difficile de faire valoir aux ingénieurs que nous n'avons pas besoin de fonctionnalités performantes et faciles à utiliser qui existent depuis l'aube du langage.

Sans plan pour prendre en charge goto , WASM est une cible de compilation de jouets, et ce n'est pas grave, c'est peut-être ainsi que le W3C voit le Web. J'espère que WASM en tant que norme atteindra plus haut, hors de l'espace d'adressage 32 bits, et entrera dans la course à la compilation. J'espère que le discours d'ingénierie pourra s'éloigner de "ce n'est pas possible..." pour accélérer les extensions GCC C telles que les étiquettes en tant que valeurs, car WASM devrait être IMPRESSIONNANT. Personnellement, TCC est considérablement plus impressionnant et plus utile à ce stade, sans tout le pontificat inutile, sans la page de destination hipster et le logo brillant.

@d4tocchini :

Selon la norme de codage SEI CERT C Rec. « Envisagez d'utiliser une chaîne goto lorsque vous laissez une fonction en erreur lors de l'utilisation et de la libération de ressources » ;

De nombreuses fonctions nécessitent l'allocation de plusieurs ressources. Un échec et un retour quelque part au milieu de cette fonction sans libérer toutes les ressources allouées pourraient produire une fuite de mémoire. C'est une erreur courante d'oublier de libérer une (ou toutes) des ressources de cette manière, donc une chaîne goto est le moyen le plus simple et le plus propre d'organiser les sorties tout en préservant l'ordre des ressources libérées.

La recommandation propose ensuite un exemple avec la solution POSIX C préférée utilisant goto . Les opposants souligneront que goto est toujours considéré comme nuisible . Fait intéressant, cette opinion n'est pas incarnée dans l'une de ces normes de codage particulières, juste une note. Ce qui nous amène au canari, le "considéré comme nuisible".

L'exemple donné dans cette recommandation peut être directement exprimé avec des ruptures étiquetées, qui sont disponibles dans Wasm. Il n'a pas besoin de la puissance supplémentaire du goto arbitraire. (C ne fournit pas de pause étiquetée et continue, doit donc se replier pour aller à plus souvent que nécessaire.)

@rossberg , bon point sur les ruptures étiquetées dans cet exemple, mais je ne suis pas d'accord avec votre hypothèse qualitative selon laquelle C doit "replier". goto est une construction plus riche que les breaks étiquetés. Si C doit être inclus parmi les cibles de compilation portables et que C ne prend pas en charge les coupures étiquetées, c'est plutôt un point muet. Java a étiqueté pauses/continues alors que Python a rejeté la fonctionnalité proposée , et étant donné que la JVM sun et le CPython par défaut sont écrits en C, ne convenez-vous pas que C en tant que langage pris en charge devrait être plus haut sur la liste des priorités ?

Si goto doit être si facilement écarté de la considération, les centaines d'utilisations de goto dans la source d'emscripten devraient-elles également être reconsidérées ?

Existe-t-il un langage qui ne peut pas être écrit en C ? C en tant que langage devrait informer les fonctionnalités de WASM. Si POSIX C n'est pas possible avec le WASM d'aujourd'hui, alors il y a votre feuille de route appropriée.

Pas vraiment sur le thème de l'argumentation, mais pour ne pas mettre de l'ombre que les erreurs aléatoires se cachent ici et là dans l'argumentation en général :

Python a des pauses étiquetées

Peux-tu élaborer? (Aka : Python n'a pas de coupures étiquetées.)

@pfalcon , oui mon mauvais, j'ai édité mon commentaire pour clarifier python proposé des pauses/continues étiquetées et je l'ai

Si goto doit être si facilement écarté de la considération, les centaines d'utilisations de goto dans la source d'emscripten devraient-elles également être reconsidérées ?

1) Notez combien cela est présent dans musl libc, pas directement dans emscripten. (Le deuxième plus utilisé est tests/third_party)
2) Les constructions au niveau de la source ne sont pas les mêmes que les instructions de bytecode
3) Emscripten n'est pas au même niveau d'abstraction que la norme wasm, donc, non, il ne devrait pas être reconsidéré sur cette base.

Plus précisément, il pourrait être utile aujourd'hui de réécrire les gotos hors de la libc, car nous aurions alors plus de contrôle sur le cfg résultant que de faire confiance à relooper/cfgstackify pour bien le gérer. Nous ne l'avons pas fait parce que c'est une quantité de travail non négligeable de se retrouver avec un code extrêmement divergent de musl en amont.

Les développeurs d'Emscripten (la dernière fois que j'ai vérifié) ont tendance à être d'avis qu'une structure de type goto serait vraiment bien, pour ces raisons évidentes, il est donc peu probable qu'elle l'abandonne, même s'il faut des années pour parvenir à un compromis acceptable.

une telle position de l'organisme de normalisation que le feu vert <marquee> est au-delà de la pâleur.

C'est une déclaration particulièrement stupide.

1) Nous, l'Internet au sens large, sommes à plus d'une décennie d'avoir pris cette décision
2) We-the-wasm-CG est un groupe de personnes entièrement (presque ?) distinct de cette balise, et est probablement aussi individuellement agacé par des erreurs passées évidentes.

sans toute la pontification gaspillée, sans la page de destination hipster et le logo brillant.

Cela aurait pu être reformulé en "Je suis frustré" sans rencontrer de problèmes de ton.

Comme le montre ce fil, ces conversations sont déjà assez difficiles.

Il y a un nouveau niveau de profonde préoccupation lorsque vous souhaitez réécrire un ensemble de fonctions profondément fiables et comprises pour tous les nouveaux, simplement parce qu'un environnement pour leur utilisation doit passer par des étapes supplémentaires pour le prendre en charge. (bien que je sois toujours dans le camp fermement s'il vous plaît-add-goto parce que je déteste être lié à l'utilisation d'un seul compilateur spécifique)

Je pense que ce fil a dépassé le stade de la productivité - il fonctionne depuis plus de quatre ans maintenant et il semble que tous les arguments possibles pour et contre des goto arbitraires aient été utilisés ici ; il convient également de noter qu'aucun de ces arguments n'est particulièrement nouveau ;)

Il existe des environnements d'exécution gérés qui ont choisi de ne pas avoir d'étiquettes de saut arbitraires, ce qui a bien fonctionné pour eux. De plus, il existe des systèmes de programmation où les sauts arbitraires sont autorisés et ils fonctionnent bien aussi. En fin de compte, les auteurs d'un système de programmation font des choix de conception et seul le temps montre vraiment si ces choix sont réussis ou non.

Les choix de conception de Wasm qui interdisent les sauts arbitraires sont au cœur de sa philosophie. Il est peu probable qu'il puisse prendre en charge les goto sans quelque chose comme des funclets, pour les mêmes raisons qu'il ne prend pas en charge les sauts indirects purs.

Les choix de conception de Wasm qui interdisent les sauts arbitraires sont au cœur de sa philosophie. Il est peu probable qu'il puisse prendre en charge les gotos sans quelque chose comme les funclets, pour les mêmes raisons qu'il ne prend pas en charge les sauts indirects purs.

@penzn Pourquoi la proposition de funclets est-

Si nous discutions d'un projet open source banal, j'en aurai fini avec ça. Nous parlons ici d'une norme de monopole de grande envergure. Une réponse communautaire vigoureuse doit être cultivée parce que nous nous soucions de nous.

@J0eCool

  1. Notez combien cela est présent dans musl libc, pas directement dans emscripten. (Le deuxième plus utilisé est tests/third_party)

Oui, le clin d'œil était à quel point il est utilisé en C en général.

  1. Les constructions au niveau de la source ne sont pas les mêmes que les instructions de bytecode

Bien sûr, ce dont nous discutons est une préoccupation interne qui a un impact sur les constructions au niveau de la source. Cela fait partie de la frustration, la boîte noire ne doit pas laisser échapper ses inquiétudes.

  1. Emscripten n'est pas au même niveau d'abstraction que la norme wasm, donc, non, il ne devrait pas être reconsidéré sur cette base.

Le fait était que vous trouverez des goto dans la majorité des projets C importants, même au sein de la chaîne d'outils WebAssembly en général. Une cible de compilateur portable pour les langages en général qui n'est pas assez expressive pour cibler ses propres compilateurs n'est pas exactement conforme à la nature de notre entreprise.

Plus précisément, il pourrait être utile aujourd'hui de réécrire les gotos hors de la libc, car nous aurions alors plus de contrôle sur le cfg résultant que de faire confiance à relooper/cfgstackify pour bien le gérer.

C'est circulaire. Beaucoup ci-dessus ont soulevé de sérieuses questions sans réponse concernant l'infaillibilité d'une telle exigence.

Nous ne l'avons pas fait parce que c'est une quantité de travail non négligeable de se retrouver avec un code extrêmement divergent de musl en amont.

Il est possible de supprimer des gotos, comme tu dis, c'est une somme de travail non négligeable ! Suggérez-vous à tout le monde de diverger sauvagement des chemins de code, car les gotos ne devraient pas être pris en charge ?

Les développeurs d'Emscripten (la dernière fois que j'ai vérifié) ont tendance à être d'avis qu'une structure de type goto serait vraiment bien, pour ces raisons évidentes, il est donc peu probable qu'elle l'abandonne, même s'il faut des années pour parvenir à un compromis acceptable.

Une lueur d'espoir ! Je serais satisfait si le support goto/label était pris au sérieux avec un élément de feuille de route + une invitation officielle pour faire bouger les choses, même si des années plus tard.

C'est une déclaration particulièrement stupide.

Vous avez raison. Pardonnez l'hyperbole, je suis un peu frustré. J'adore wasm et je l'utilise souvent, mais je vois finalement un chemin douloureux devant moi si je veux faire quelque chose de remarquable avec, comme le port TCC. Après avoir lu tous les commentaires et articles, je n'arrive toujours pas à savoir si l'opposition est technique, philosophique ou politique. Comme @neelance l'a exprimé,

« Quelqu'un en charge de V8 pourrait-il confirmer dans ce fil que l'opposition au flux de contrôle irréductible n'est pas influencée par la mise en œuvre actuelle de V8 ?

Je demande parce que c'est ce qui me dérange le plus. [...]

Si vous en écoutez, prenez à cœur les commentaires de @neelance concernant Go 1.11. C'est difficile à discuter. Bien sûr, nous pouvons tous faire le dépoussiérage non trivial de goto, mais même dans ce cas, nous prenons un sérieux coup de perf qui ne peut être corrigé qu'avec une instruction goto.

Encore une fois, pardonnez ma frustration, mais si ce problème est clos sans adresse appropriée, alors je crains qu'il n'enverra le mauvais type de signal qui ne fera qu'exaspérer ce genre de réponses de la communauté et est inapproprié pour l'un des plus grands efforts de normalisation de notre domaine. Il va sans dire que je suis un grand fan et supporter de tous les membres de cette équipe. Merci!

Voici un autre problème du monde réel causé par l'absence de goto/funclets : https://github.com/golang/go/issues/42979

Pour ce programme, le compilateur Go génère actuellement un binaire wasm avec 18 000 block imbriqués. Le binaire wasm lui-même a une taille de 2,7 Mo, mais lorsque je l'exécute sur wasm2wat j'obtiens un fichier .wat de 4,7 Go. ??

Je pourrais essayer de donner au compilateur Go une heuristique afin qu'au lieu d'une seule énorme table de saut, il puisse créer une sorte d'arbre binaire, puis examiner la variable cible de saut plusieurs fois. Mais est-ce vraiment comme ça que ça doit être avec wasm ?

Je voudrais ajouter que je trouve étrange la façon dont les gens semblent penser que c'est parfaitement bien si un seul compilateur (Emscripten[1]) peut prendre en charge de manière réaliste WebAssembly.
Cela me rappelle un peu la situation de libopus (une norme qui dépend normativement du code protégé par le droit d'auteur).

Je trouve aussi étrange comment WebAssembly devs semblent être si avec véhémence contre cela, en dépit de tout le monde à peu près de la fin du compilateur des choses en leur disant qu'il est nécessaire. N'oubliez pas : WebAssembly est un standard, pas un manifeste. Et le fait est que la plupart des compilateurs modernes utilisent une forme de blocs de base SSA + en interne (ou quelque chose de presque équivalent, avec les mêmes propriétés), qui n'ont aucun concept de boucles explicites[2]. Même les JIT utilisent quelque chose de similaire, c'est à quel point c'est courant.
L'exigence absolue pour que la rebouclage se produise sans échappatoire de "juste utiliser goto" est, à ma connaissance[3], sans précédent en dehors des traducteurs de langue à langue --- et même alors, seuls les traducteurs de langue à langue qui cibler les langues sans goto. En particulier, je n'ai jamais entendu dire que cela devait être fait pour une sorte d'IR ou de bytecode, autre que WebAssembly.

Il est peut-être temps de renommer WebAssembly en WebEmscripten (WebScripten ?).

Comme @d4tocchini l'a dit, s'il n'y avait pas eu le statut monopolistique de WebAssembly (nécessaire, en raison de la situation de normalisation), il aurait probablement déjà été transformé en quelque chose qui peut raisonnablement prendre en charge ce que les développeurs du compilateur savent déjà qu'il doit prendre en charge.
Et non, "juste utiliser emscripten" n'est pas un contre-argument valide, car il fait dépendre la norme d'un seul fournisseur de compilateur. J'espère que je n'ai pas besoin de vous dire pourquoi c'est mauvais.

EDIT : j'ai oublié d'ajouter une chose :
Vous n'avez toujours pas précisé si la question est technique, philosophique ou politique. Je soupçonne ce dernier, mais il serait volontiers démenti (parce que les problèmes techniques et philosophiques peuvent être résolus beaucoup plus facilement que politiques).

Voici un autre problème du monde réel causé par l'absence de goto/funclets : golang/go#42979

Pour ce programme, le compilateur Go génère actuellement un binaire wasm avec 18 000 block imbriqués. Le binaire wasm lui-même a une taille de 2,7 Mo, mais lorsque je l'exécute sur wasm2wat j'obtiens un fichier .wat de 4,7 Go. ??

Je pourrais essayer de donner au compilateur Go une heuristique afin qu'au lieu d'une seule énorme table de saut, il puisse créer une sorte d'arbre binaire, puis examiner la variable cible de saut plusieurs fois. Mais est-ce vraiment comme ça que ça doit être avec wasm ?

Cet exemple est vraiment intéressant. Comment un programme en ligne droite aussi simple génère-t-il ce code ? Quelle est la relation entre le nombre d'éléments du tableau et le nombre de blocs ? En particulier, dois-je interpréter cela comme signifiant que chaque accès à un élément de tableau nécessite que des blocs _multiples_ soient compilés fidèlement ?

Et non, "juste utiliser emscripten" n'est pas un contre-argument valable

Je pense que le véritable contre-argument dans cette veine serait qu'un autre compilateur souhaitant cibler Wasm peut/doit implémenter son propre algorithme de type relooper. Personnellement, je pense que Wasm devrait éventuellement avoir une boucle multi-corps (proche des funclets) ou quelque chose de similaire qui serait une cible naturelle pour goto .

@conrad-watt Plusieurs facteurs font que chaque affectation utilise plusieurs blocs de base dans le CFG. L'un d'eux est qu'il y a un contrôle de longueur sur la tranche car la longueur n'est pas connue au moment de la compilation. En général, je dirais que les compilateurs considèrent les blocs de base comme une construction relativement bon marché, mais avec wasm, ils sont assez chers, surtout dans ce cas particulier.

@neelance dans l'exemple modifié où le code est divisé entre plusieurs fonctions, la surcharge de mémoire (exécution/compilation) s'avère beaucoup plus faible. Est-ce que moins de blocs sont générés dans ce cas, ou est-ce simplement que les fonctions séparées signifient que le GC du moteur peut être plus granulaire ?

@conrad-watt Ce n'est même pas le code Go qui utilise la mémoire, mais l'hôte WebAssembly : lorsque j'instancie le binaire wasm avec Chrome 86, mon CPU passe à 100% pendant 2 minutes et l'utilisation de la mémoire de l'onglet culmine à 11,3 Go. C'est avant que le code wasm/Go soit exécuté. C'est la forme du binaire wasm qui est à l'origine du problème.

C'était déjà ma compréhension. Je m'attendrais à ce qu'un grand nombre de blocs/annotations de type entraîne une surcharge de mémoire spécifiquement pendant la compilation/l'instanciation.

Pour essayer de lever l'ambiguïté de ma question précédente - si la version fractionnée du code se compile en Wasm avec moins de blocs (à cause d'une bizarrerie de relooper), ce serait une explication de la réduction de la surcharge de mémoire et serait une bonne motivation pour ajouter des informations plus générales flux de contrôle vers Wasm.

Alternativement, il se peut que le code divisé donne (à peu près) le même nombre total de blocs, mais parce que chaque fonction est compilée séparément JIT, les métadonnées/IR utilisées pour compiler chaque fonction peuvent être plus facilement GC par le moteur Wasm . Un problème similaire s'est produit dans la V8 il y a des années lors de l'analyse/compilation de fonctions asm.js volumineuses. Dans ce cas, l'introduction d'un flux de contrôle plus général dans Wasm ne résoudrait pas le problème.

Tout d'abord, j'aimerais clarifier : le compilateur Go n'utilise pas l'algorithme relooper, car il est intrinsèquement incompatible avec le concept de commutation de goroutines. Tous les blocs de base sont exprimés via une table de saut avec un peu de chute si possible.

Je suppose qu'il y a une croissance exponentielle de la complexité dans l'environnement d'exécution wasm de Chrome en ce qui concerne la profondeur des block imbriqués. La version split a le même nombre de blocs mais une profondeur maximale plus petite.

Dans ce cas, l'introduction d'un flux de contrôle plus général dans Wasm ne résoudrait pas le problème.

Je suis d'accord que ce problème de complexité peut probablement être résolu à la fin de Chrome. Mais j'aime toujours poser la question "Pourquoi ce problème a-t-il existé en premier lieu ?". Je dirais qu'avec un flux de contrôle plus général, ce problème n'aurait jamais existé. En outre, il existe toujours une surcharge de performances générale importante en raison du fait que tous les blocs de base sont exprimés sous forme de tables de saut, ce qui, à mon avis, ne disparaîtra probablement pas par optimisation.

Je suppose qu'il y a une croissance exponentielle de la complexité dans le runtime wasm de Chrome en ce qui concerne la profondeur des blocs imbriqués. La version split a le même nombre de blocs mais une profondeur maximale plus petite.

Cela signifie-t-il que dans une fonction en ligne droite avec N accès au tableau, l'accès final au tableau sera imbriqué (un facteur constant de) N blocs de profondeur ? Si tel est le cas, existe-t-il un moyen de réduire cela en factorisant différemment le code de gestion des erreurs ? Je m'attendrais à ce que n'importe quel compilateur analyse s'il doit analyser 3000 boucles imbriquées (analogie très approximative), donc si cela est inévitable pour des raisons sémantiques, ce serait également un argument pour un flux de contrôle plus général.

Si la différence d'imbrication est moins flagrante que cela, mon intuition serait que V8 ne fait presque pas de GC des métadonnées _pendant_ la compilation d'une seule fonction Wasm, donc même si nous avions quelque chose comme une proposition de funclets modifiée dans le langage dès le début , les mêmes frais généraux seraient toujours visibles sans qu'ils fassent d'intéressantes optimisations GC.

En outre, il existe toujours une surcharge de performances générale importante en raison du fait que tous les blocs de base sont exprimés sous forme de tables de saut, ce qui, à mon avis, ne disparaîtra probablement pas par optimisation.

Convenez qu'il est clairement préférable (d'un point de vue purement technique) d'avoir ici une cible plus naturelle.

Cela signifie-t-il que dans une fonction en ligne droite avec N accès au tableau, l'accès final au tableau sera imbriqué (un facteur constant de) N blocs de profondeur ? Si tel est le cas, existe-t-il un moyen de réduire cela en factorisant différemment le code de gestion des erreurs ? Je m'attendrais à ce que n'importe quel compilateur analyse s'il doit analyser 3000 boucles imbriquées (analogie très approximative), donc si cela est inévitable pour des raisons sémantiques, ce serait également un argument pour un flux de contrôle plus général.

L'inverse : la première affectation est imbriquée aussi profondément, pas la dernière. Des block imbriqués et un seul br_table en haut sont la façon dont une instruction switch traditionnelle est exprimée en wasm. C'est la table de saut que j'ai mentionnée. Il n'y a pas 3000 boucles imbriquées.

Si la différence d'imbrication est moins flagrante que cela, mon intuition serait que V8 ne fait presque pas de GC de métadonnées lors de la compilation d'une seule fonction Wasm, donc même si nous avions quelque chose comme une proposition de funclets modifiée dans le langage dès le début , les mêmes frais généraux seraient toujours visibles sans qu'ils fassent d'intéressantes optimisations GC.

Oui, il peut également y avoir une implémentation qui a une complexité exponentielle en ce qui concerne le nombre de blocs de base. Mais gérer des blocs de base (même en grande quantité) est ce que font beaucoup de compilateurs toute la journée. Par exemple, le compilateur Go lui-même gère facilement ce nombre de blocs de base lors de sa compilation, même s'ils sont traités par plusieurs passes d'optimisation.

Oui, il peut également y avoir une implémentation qui a une complexité exponentielle en ce qui concerne le nombre de blocs de base. Mais gérer des blocs de base (même en grande quantité) est ce que font beaucoup de compilateurs toute la journée. Par exemple, le compilateur Go lui-même gère facilement ce nombre de blocs de base lors de sa compilation, même s'ils sont traités par plusieurs passes d'optimisation.

Bien sûr, mais un problème de performances ici serait orthogonal à la façon dont le flux de contrôle entre ces blocs de base est exprimé dans le langage source d'origine (c'est-à-dire qu'il ne s'agit pas d'une motivation pour un flux de contrôle plus général dans Wasm). Pour voir si V8 est particulièrement mauvais ici, on pourrait vérifier si FireFox/SpiderMonkey ou Lucet/Cranelift présentent les mêmes frais généraux de compilation.

J'ai fait d'autres tests : Firefox et Safari ne présentent aucun problème. Fait intéressant, Chrome est même capable d'exécuter le code avant la fin du processus intensif, il semble donc qu'une tâche non strictement nécessaire pour exécuter le binaire wasm pose un problème de complexité.

Bien sûr, mais un problème de performances ici serait orthogonal à la façon dont le flux de contrôle entre ces blocs de base est exprimé dans la langue source d'origine.

Je vois ce que tu veux dire.

Je crois toujours que représenter des blocs de base non pas via des instructions de saut mais via une variable de saut et une énorme table de saut / blocs imbriqués exprime le concept simple de blocs de base d'une manière assez complexe. Cela entraîne une surcharge de performances et un risque de problèmes de complexité tels que celui que nous avons vu ici. Je crois que les systèmes plus simples sont meilleurs et plus robustes que les systèmes complexes. Je n'ai toujours pas vu d'arguments qui me convainquent que le système plus simple est un mauvais choix. J'ai seulement entendu dire que V8 aurait du mal à mettre en œuvre un flux de contrôle arbitraire et ma question ouverte pour me dire que cette déclaration est fausse (https://github.com/WebAssembly/design/issues/796#issuecomment-623431527) n'a pas n'a pas encore été répondu.

@neelance

Chrome est même capable d'exécuter le code avant la fin du processus intensif

Il semble que le compilateur de base Liftoff soit ok, et le problème réside dans le compilateur d'optimisation TurboFan. Veuillez déposer un problème, ou veuillez fournir un cas de test et je peux en déposer un si vous préférez.

Plus généralement : Pensez-vous que les plans de changement de pile Wasm seront capables de résoudre les problèmes d'implémentation de goroutine de Go ? C'est le meilleur lien que j'ai pu trouver, mais il est assez actif maintenant, avec une réunion bi-hebdomadaire, et plusieurs cas d'utilisation forts qui motivent le travail. Si Go peut utiliser les coroutines wasm pour éviter le grand modèle de commutation, je pense que des gotos arbitraires ne seraient pas nécessaires.

Le compilateur Go n'utilise pas l'algorithme relooper, car il est intrinsèquement incompatible avec le concept de commutation de goroutines.

C'est vrai qu'il ne s'applique pas tout seul. Cependant, nous avons de bons résultats avec l'utilisation du flux de contrôle structuré wasm +

Je serais très heureux d'expérimenter ça sur Go, si ça vous intéresse ! Ce ne serait évidemment pas aussi bon que le support de commutation de pile intégré dans wasm, mais cela pourrait déjà être mieux que le grand modèle de commutation. Et il serait plus facile de passer plus tard à la prise en charge de la commutation de pile intégrée. Concrètement, comment cette expérience pourrait fonctionner est de faire en sorte que Go émette du code normalement structuré, sans se soucier du tout du changement de pile, et émet simplement un appel à une fonction spéciale maybe_switch_goroutine aux points appropriés. La transformation Asyncify s'occuperait essentiellement de tout le reste.

Je m'intéresse aux gotos pour les émulateurs de recompilation dynamique tels que qemu. Contrairement à d'autres compilateurs, qemu n'a à aucun moment connaissance de la structure du flux de contrôle du programme, et les gotos sont donc la seule cible raisonnable. Les tailcalls pourraient résoudre ce problème, en compilant chaque bloc en tant que fonction et chaque goto en tant que tailcall.

@kripken Merci pour votre message très utile.

Il semble que le compilateur de base Liftoff soit ok, et le problème réside dans le compilateur d'optimisation TurboFan. Veuillez déposer un problème, ou veuillez fournir un cas de test et je peux en déposer un si vous préférez.

Voici un binaire wasm que vous pouvez exécuter avec wasm_exec.html .

Pensez-vous que les plans de changement de pile Wasm seront en mesure de résoudre les problèmes d'implémentation de goroutine de Go ?

Oui, à première vue, il semble que cela aiderait.

Cependant, nous avons de bons résultats avec l'utilisation du flux de contrôle structuré wasm + Asyncify.

Cela semble également prometteur. Nous aurions besoin d'implémenter le relooper dans Go, mais ça va, je suppose. Un petit inconvénient est qu'il ajoute une dépendance à binaryen pour produire des binaires wasm. Je vais probablement écrire une proposition bientôt.

Je pense que l'algorithme de stackifier de LLVM est plus facile/meilleur, au cas où vous voudriez l'implémenter : https://medium.com/leaningtech/solving-the-structured-control-flow-problem-once-and-for-all-5123117b1ee2

J'ai déposé une proposition pour le projet Go : https://github.com/golang/go/issues/43033

@neelance , agréable de voir que la suggestion de

Si l'argument de « bon goût » de Linus Torvalds pour les listes chaînées repose sur l'élégance de supprimer une seule déclaration de branche à cas particulier, il est difficile de voir ce genre de gymnastique à cas particulier comme une victoire ou même un pas dans la bonne direction. Ayant personnellement utilisé des gotos pour des API de type async en C, parler de la commutation de pile avant que les instructions goto ne déclenchent toutes sortes d'odeurs.

Veuillez me corriger si j'ai mal lu, mais à part des réponses apparemment rapides axées sur des particularités marginales à certaines questions soulevées, il semble que les responsables ici n'aient pas apporté de clarté sur le sujet ni répondu aux questions difficiles. Avec tout le respect que je vous dois, cette ossification lente n'est-elle pas la marque de fabrique de la politique d'entreprise insensible ? Si tel est le cas, je comprends le sort... Imaginez tous les langages/compilateurs que la marque Wasm pourrait se vanter de prendre en charge si seulement ANSI C était un test décisif compatible !

@neelance @darkuranium @d4tocchini tous les contributeurs de pensent pas que l'absence de goto est la bonne chose, en fait, je le qualifierais personnellement d'erreur de conception n ° 1 de Wasm. Je suis absolument en faveur de l'ajouter (soit sous forme de funclets ou directement).

Cependant, débattre sur ce fil ne va pas faire arriver les gotos, et ne va pas comme par magie convaincre toutes les personnes impliquées dans Wasm et faire le travail pour vous. Voici les étapes à suivre :

  1. Rejoignez le Wasm CG.
  2. Quelqu'un investit du temps pour devenir le champion d'une proposition goto. Je recommande de partir de la proposition de funclets existante, car elle a déjà été bien pensée par @sunfishcode pour être le "moins intrusif" pour les moteurs et outils actuels qui reposent sur la structure de blocs, il a donc plus de chances de réussir qu'un raw aller à.
  3. Aidez-le à franchir les 4 étapes de la proposition. Cela inclut de faire de bonnes conceptions pour toutes les objections qui vous sont adressées, d'engager des discussions, dans le but de satisfaire suffisamment de personnes pour que vous obteniez des votes majoritaires lorsque vous avancez dans les étapes.

@d4tocchini Honnêtement, je considère actuellement les solutions suggérées comme "la meilleure voie à suivre étant donné les circonstances que je ne peux pas changer", alias "solution de contournement". Je considère toujours les instructions jump/goto (ou funclets) comme le moyen le plus simple et donc préférable. (Encore merci à @kripken pour avoir suggéré utilement les alternatives.)

@aardappel Pour autant que je sache, @sunfishcode a essayé de pousser la proposition de funclets et a échoué. Pourquoi serait-ce différent pour moi ?

@neelance Je ne pense pas que @sunfishcode ait eu beaucoup de temps pour pousser la proposition au-delà de sa création initiale, elle est donc « bloquée » plutôt que « échouée ». Comme j'essayais de l'indiquer, il faut qu'un champion fasse un travail continu pour qu'une proposition parvienne jusqu'au bout du pipeline.

@neelance

Merci pour le test ! Je peux confirmer le même problème localement. J'ai déposé https://bugs.chromium.org/p/v8/issues/detail?id=11237

Nous aurions besoin d'implémenter le relooper dans Go [..] Un petit inconvénient est qu'il ajoute une dépendance à binaryen pour produire des binaires wasm.

Au fait, si cela peut aider, nous pouvons créer une bibliothèque de binaryen sous la forme d'un seul fichier C. C'est peut-être plus facile à intégrer ?

De plus, en utilisant Binaryen, vous pouvez utiliser l'implémentation Relooper qui est là . Vous pouvez lui transmettre des blocs IR de base et le laisser faire le rebouclage.

@taralx

Je pense que l'algorithme de stackifier de LLVM est plus facile/meilleur,

Notez que ce lien ne concerne pas LLVM en amont, c'est le compilateur Cheerp (qui est un fork de LLVM). Leur Stackifier a un nom similaire à celui de LLVM, mais il est différent.

Notez également que cette publication Cheerp fait référence à l'algorithme d'origine de 2011 - l' implémentation moderne de relooper (comme mentionné précédemment) n'a pas eu les problèmes mentionnés depuis de nombreuses années. Je ne connais pas d'alternative plus simple ou meilleure à cette approche générale, qui est très similaire à ce que font Cheerp et d'autres - ce sont des variations sur un thème.

@kripken Merci d'avoir

Au fait, si cela peut aider, nous pouvons créer une bibliothèque de binaryen sous la forme d'un seul fichier C. C'est peut-être plus facile à intégrer ?

Peu probable. Le compilateur Go lui-même a été converti en Go pur il y a quelque temps et il n'utilise aucune autre dépendance C. Je ne pense pas que ce sera une exception.

Voici l'état actuel de la proposition de funclets : la prochaine étape du processus consiste à demander un vote du CG pour entrer dans l'étape 1.

Je me concentre actuellement sur d'autres domaines de WebAssembly et je n'ai pas la bande passante pour faire avancer les fonctions ; si quelqu'un est intéressé à assumer le rôle de champion pour les funclets, je serais heureux de le lui transmettre.

Peu probable. Le compilateur Go lui-même a été converti en Go pur il y a quelque temps et il n'utilise aucune autre dépendance C. Je ne pense pas que ce sera une exception.

En outre, cela ne résout pas le problème de l'utilisation intensive de relooper provoquant de graves problèmes de performances dans les environnements d'exécution WebAssembly.

@Vurich

Je pense que cela pourrait être le meilleur cas pour ajouter des gotos à wasm, mais quelqu'un aurait besoin de collecter des données convaincantes à partir de code du monde réel montrant de telles falaises de performances. Je n'ai pas vu de telles données moi-même. Les travaux d'analyse des déficits de performance wasm tels que « Pas si rapide : Analyse des performances de WebAssembly par rapport au code natif » (2019) ne prennent pas non plus en charge le flux de contrôle comme facteur important (ils notent une plus grande quantité d'instructions de branchement, mais ce ne sont

@kripken Avez-vous des suggestions sur la façon dont on pourrait collecter de telles données ? Comment montrer qu'un déficit de perf est dû à un flux de contrôle structuré ?

Il est peu probable qu'il y ait beaucoup de travail pour analyser les performances de l'étape de compilation, ce qui fait partie de la plainte ici.

Je suis quelque peu surpris que nous n'ayons pas encore de construction de cas de commutation, mais les funclets la subsume.

@neelance

Ce n'est pas facile de comprendre les causes spécifiques, oui. Pour, par exemple, les vérifications des limites, vous pouvez simplement les désactiver dans la VM et mesurer cela, mais il n'y a malheureusement pas de moyen simple de faire la même chose pour les gotos.

Une option consiste à comparer manuellement le code machine émis, ce qu'ils ont fait dans ce document lié.

Une autre option consiste à compiler le wasm en quelque chose qui, selon vous, peut gérer le flux de contrôle de manière optimale, c'est-à-dire "annuler" la structuration. LLVM devrait pouvoir le faire, donc exécuter wasm dans une VM qui utilise LLVM (comme WAVM ou wasmer) ou via WasmBoxC pourrait être intéressant. Vous pourriez peut-être désactiver les optimisations CFG dans LLVM et voir à quel point cela compte.

@taralx

Intéressant, ai-je raté quelque chose sur les temps de compilation ou l'utilisation de la mémoire ? Le flux de contrôle structuré devrait en fait être meilleur là-bas - par exemple, il est très simple d'accéder au formulaire SSA à partir de celui-ci, par rapport à un CFG général. C'était en fait l'une des raisons pour lesquelles wasm a opté pour un flux de contrôle structuré en premier lieu. Cela est également mesuré très attentivement car cela affecte les temps de chargement sur le Web.

(Ou voulez-vous dire les performances du compilateur sur la machine du développeur ? Il est vrai que wasm penche dans le sens de faire plus de travail là-bas, et moins sur le client.)

Je voulais dire les performances de compilation dans l'embedder, mais il semble que cela soit traité comme un bogue , pas nécessairement un pur problème de performances ?

@taralx

Oui, je pense que c'est un bug. Cela se produit simplement dans un niveau sur une machine virtuelle. Et il n'y a pas de raison fondamentale à cela - un flux de contrôle structuré ne nécessite pas plus de ressources, il devrait en nécessiter moins. C'est-à-dire que je parierais que de tels bugs de performances seraient plus susceptibles de se produire si wasm avait des gotos.

@kripken

Le flux de contrôle structuré devrait en fait être meilleur là-bas - par exemple, il est très simple d'accéder au formulaire SSA à partir de celui-ci, par rapport à un CFG général. C'était en fait l'une des raisons pour lesquelles wasm a opté pour un flux de contrôle structuré en premier lieu. Cela est également mesuré très attentivement car cela affecte les temps de chargement sur le Web.

Une question très spécifique, juste au cas où : Connaissez-vous un compilateur Wasm qui fait cela - "très simple" allant du "flux de contrôle structuré" à la forme SSA. Parce que d'un coup d'œil, le flux de contrôle de Wasm n'est pas (entièrement/en fin de compte) structuré. Le contrôle formellement structuré est celui où il n'y a pas de break s, continue s, return s (en gros, le modèle de programmation de Scheme, sans magie comme call/cc). Lorsque ceux-ci sont présents, un tel flux de contrôle peut en gros être appelé « semi-structuré ».

Il existe un algorithme SSA bien connu pour un flux de contrôle entièrement structuré : http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.45.4503 . Voici ce qu'il a à dire sur le flux de contrôle semi-structuré :

Pour les instructions structurées, nous avons montré comment générer à la fois la forme SSA et l'arbre dominant en un seul passage pendant l'analyse. Dans la section suivante, nous montrerons qu'il est même possible d'étendre notre méthode à une certaine classe d'instructions non structurées (LOOP/EXIT et RETURN) qui peuvent provoquer des sorties des structures de contrôle à des points arbitraires. Cependant, étant donné que de telles sorties sont une sorte de goto (discipliné), il n'est pas surprenant qu'elles soient beaucoup plus difficiles à gérer que les instructions structurées.

OTOH, il existe un autre algorithme bien connu, https://pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf qui sans doute aussi en un seul passage, mais qui n'a pas de problèmes non seulement avec le contrôle non structuré flux, mais même avec un flux de contrôle irréductible (bien qu'il ne produise pas un résultat optimal pour cela).

Donc, la question est à nouveau de savoir si vous savez qu'un projet a eu la peine d'étendre réellement l'algorithme Brandis/Mössenböck et a obtenu des avantages tangibles sur cette route par rapport à Braun et al. algorithme (en passant, mon intuition intuitive est que Braun algo est exactement une extension de la "limite supérieure", bien que je sois trop stupide pour me le prouver intuitivement, sans parler d'une preuve formelle, alors c'est tout - intuition intuitive ).

Et le thème général de la question est d'établir (bien que je dirais "maintenir") la raison ultime pour laquelle Wasm s'est retiré du support arbitraire de goto. Parce qu'en regardant ce fil depuis des années, le modèle mental que j'ai construit est que c'est fait pour éviter de faire face à des CFG irréductibles . Et en effet, le gouffre se situe entre les CFG réductibles et irréductibles , de nombreux algorithmes d' optimisation étant (beaucoup) plus faciles pour les CFG réductibles , et c'est ce que de nombreux optimiseurs ont codé. Le flux de contrôle (semi) structuré dans Wasm n'est qu'un moyen peu coûteux de garantir réductibilité.

La mention de toute facilité particulière de la production SSA pour les CFG structurés (et les CFG Wasm ne semblent pas vraiment être structurés au sens formel) obscurcit en quelque sorte l'image claire ci-dessus. C'est pourquoi je demande s'il existe des références spécifiques selon lesquelles la construction SSA bénéficie pratiquement du formulaire Wasm CFG.

Merci.

@kripken Je suis un peu confus en ce moment et désireux d'apprendre. Je regarde la situation et je vois actuellement ce qui suit :


La source de votre programme a un certain flux de contrôle. Ce CFG est soit réductible, soit il ne l'est pas, par exemple goto a été utilisé ou non dans la langue source. Il n'y a aucun moyen de changer ce fait. Ce CFG peut être transformé en code machine, par exemple comme le fait nativement le compilateur Go.

Si le CFG est déjà réductible, alors tout va bien et la VM wasm peut le charger rapidement. Toute passe de traduction devrait être capable de détecter qu'il s'agit d'un cas simple et d'agir rapidement. Autoriser les CFG irréductibles ne devrait pas ralentir cette affaire.

Si le CFG n'est pas réductible, alors il y a deux options :

  • Le compilateur le rend réductible, par exemple en introduisant une table de saut. Cette étape perd des informations. Il est difficile de restaurer le CFG d'origine sans avoir une analyse spécifique au compilateur qui a produit le binaire. En raison de cette perte d'informations, tout code machine généré sera un peu plus lent que le code généré à partir du CFG initial. Nous pouvons peut-être générer ce code machine avec un algorithme à un seul passage, mais c'est au prix d'une perte d'informations. [1]

  • Nous permettons au compilateur d'émettre un CFG irréductible. La VM devra peut-être la rendre réductible. Cela ralentit le temps de chargement, mais uniquement dans les cas où le CFG n'est en fait pas réductible. Le compilateur a la possibilité de choisir entre l'optimisation des performances de temps de chargement ou les performances d'exécution.

[1] Je suis conscient que ce n'est pas vraiment une perte d'information s'il y a encore un moyen d'inverser l'opération, mais je ne pourrais pas mieux le décrire.


Où est la faille dans ma réflexion ?

@pfalcon

Connaissez-vous un compilateur Wasm qui fait cela - "très simple" allant du "flux de contrôle structuré" à la forme SSA.

À propos des VM : je ne sais pas directement. Mais l'IIRC à l'époque @titzer et @lukewagner ont déclaré qu'il était pratique de mettre en œuvre de cette façon - peut-être que l'un d'entre eux peut élaborer. Je ne sais pas si l'irréductibilité était le problème ici ou non. Et je ne sais pas s'ils ont implémenté les algorithmes que vous mentionnez ou non.

À propos d'autres choses que les machines virtuelles : l'optimiseur Binaryen bénéficie certainement du flux de contrôle structuré de wasm, et pas seulement du fait qu'il est réductible. Diverses optimisations sont plus simples car on sait toujours où se trouvent les en-têtes de boucle, par exemple, qui sont annotés dans le wasm. (Les autres optimisations OTOH sont plus difficiles à faire, et nous avons également un CFG IR général pour celles-ci...)

@neelance

Si le CFG est déjà réductible, alors tout va bien et la VM wasm peut le charger rapidement. Toute passe de traduction devrait être capable de détecter qu'il s'agit d'un cas simple et d'agir rapidement. Autoriser les CFG irréductibles ne devrait pas ralentir cette affaire.

Peut-être que je ne vous comprends pas complètement. Mais le fait qu'une VM wasm puisse charger du code rapidement dépend non seulement de son caractère réductible ou non, mais aussi de la façon dont il est encodé. Concrètement, nous aurions pu imaginer un format qui soit un CFG général, puis la VM doit faire le travail pour vérifier qu'il est réductible. Wasm a choisi d'éviter ce travail - l'encodage est nécessairement réductible (c'est-à-dire que lorsque vous lisez le wasm et effectuez la validation triviale, vous prouvez également qu'il est réductible sans faire de travail supplémentaire).

De plus, l'encodage de wasm ne donne pas seulement une garantie de réductibilité sans avoir besoin de le vérifier. Il annote également les en-têtes de boucle, les ifs et d'autres choses utiles (comme je l'ai mentionné séparément plus tôt dans ce commentaire). Je ne suis pas sûr de savoir combien les machines virtuelles de production en bénéficient, mais je m'attendrais à ce qu'elles le fassent. (Peut-être surtout dans les compilateurs de base ?)

Dans l'ensemble, je pense qu'autoriser les CFG irréductibles peut ralentir le cas rapide, à moins que les irréductibles ne soient encodés de manière distincte (comme les funclets sont proposés).

@kripken

Merci pour votre explication.

Oui, c'est exactement la différenciation que j'essaie de faire : je vois l'avantage de la notation/codage structuré pour le cas CFG réductible. Mais il ne devrait pas être difficile d'ajouter une construction qui permet la notation d'un CFG irréductible tout en conservant les avantages existants dans le cas d'un CFG source réductible (par exemple si vous n'utilisez pas cette nouvelle construction, alors le CFG est garanti être réductible).

En conclusion, je ne vois pas comment on peut affirmer qu'une notation purement réductible est plus rapide. Dans le cas d'une source réductible CFG, il est tout aussi rapide. Et dans le cas d'une source irréductible CFG, on peut tout au plus affirmer qu'elle n'est pas significativement plus lente, mais quelques cas réels ont déjà montré que c'est peu probable le cas en général.

En bref, je ne vois pas comment les considérations de performances peuvent être un argument qui empêche un flux de contrôle irréductible et cela me fait me demander pourquoi la prochaine étape doit être la collecte de données de performances.

@neelance

Oui, je suis d'accord que nous pourrions ajouter une nouvelle construction - comme les funclets - et en ne l'utilisant pas, cela ne ralentirait pas le cas existant.

Mais il y a un inconvénient à ajouter une nouvelle construction car cela ajoute de la complexité à wasm. En particulier, cela signifie une plus grande surface sur les machines virtuelles, ce qui signifie plus de bugs et de problèmes de sécurité possibles. Wasm s'est penché pour avoir autant de complexité du côté du développeur que possible afin de réduire la complexité de la VM.

Certaines propositions wasm ne concernent pas seulement la vitesse, comme GC (qui permet la collecte de cycles avec JS). Mais pour les propositions qui concernent la vitesse, comme les funclets, nous devons montrer que la vitesse justifie la complexité. Nous avons eu ce débat sur SIMD qui concerne également la vitesse, et avons décidé que cela en valait la peine car nous avons vu qu'il peut atteindre de manière fiable de très grandes accélérations sur le code du monde réel (2x ou même plus).

(Il y a d'autres avantages que la vitesse à autoriser les CFG généraux, je suis d'accord, comme faciliter le ciblage de wasm par les compilateurs. Mais nous pouvons résoudre ce problème sans ajouter de complexité aux machines virtuelles wasm. Nous prenons déjà en charge les CFG arbitraires dans LLVM et Binaryen , permettant aux compilateurs d'émettre des CFG et de ne pas se soucier du flux de contrôle structuré. Si cela ne suffit pas, nous - les outils, je veux dire, moi y compris - devrions en faire plus.)

Funclet ne concerne pas tant la vitesse que la possibilité pour les langages avec un flux de contrôle non trivial de se compiler vers WebAssembly, C et Go étant les plus évidents, mais cela s'applique à tout langage asynchrone/attente. De plus, le choix d'avoir un flux de contrôle hiérarchique conduit en fait à _plus_ de bogues dans les machines virtuelles, comme en témoigne le fait que tous les compilateurs Wasm autres que V8 décomposent de toute façon le flux de contrôle hiérarchique en un CFG. Les EBB dans un CFG peuvent représenter les multiples constructions de flux de contrôle dans Wasm et plus, et avoir une seule construction à compiler conduit à beaucoup moins de bogues que d'avoir de nombreux types différents avec des utilisations différentes.

Même Lightbeam, un compilateur de streaming très simple, a constaté une diminution massive des bogues de mauvaise compilation après avoir ajouté une étape de traduction supplémentaire qui décomposait le flux de contrôle en un CFG. Cela va doubler pour l'autre côté de ce processus - Relooper est beaucoup plus sujet aux erreurs que l'émission de funclets, et des développeurs travaillant sur les backends Wasm pour LLVM et d'autres compilateurs qui étaient des funclets à implémenter m'ont dit qu'ils émettraient chaque fonction en utilisant uniquement des funclets, afin d'améliorer la fiabilité et la simplicité de codegen. Tous les compilateurs produisant Wasm utilisent des EBB, tous sauf un des compilateurs utilisant Wasm utilisent des EBB, ce refus d'implémenter des funclets ou une autre façon de représenter les CFG ajoute simplement une étape avec perte entre les deux qui nuit à toutes les parties impliquées autres que l'équipe V8 .

"Flux de contrôle irréductible considéré comme nuisible" n'est qu'un sujet de discussion, vous pouvez facilement ajouter la restriction selon laquelle le flux de contrôle des funclets doit être réductible, puis si vous souhaitez autoriser un flux de contrôle irréductible à l'avenir, tous les modules Wasm existants avec un flux de contrôle réductible fonctionneraient sans modification sur un moteur qui prend en charge en outre un flux de contrôle irréductible. Il s'agirait simplement de supprimer le contrôle de réductibilité dans le validateur.

@Vurich

Vous pouvez facilement ajouter la restriction que le flux de contrôle des funclets soit réductible

Vous pouvez, mais ce n'est pas anodin - les machines virtuelles devraient le vérifier. Je ne pense pas que cela soit possible en un seul passage linéaire, ce qui serait un problème pour les compilateurs de base, qui sont désormais présents dans la plupart des machines virtuelles. (En fait, le simple fait de trouver des backedges de boucle - ce qui est un problème plus simple, et nécessaire pour d'autres raisons également - ne peut pas être fait en une seule passe avant, n'est-ce pas ?)

tous les compilateurs Wasm autres que V8 décomposent de toute façon le flux de contrôle hiérarchique en un CFG.

Faites-vous référence à l'approche « mer de nœuds » utilisée par TurboFan ? Je ne suis pas un expert là-dessus donc je laisse aux autres le soin de répondre.

Mais plus généralement, même si vous n'achetez pas l'argument ci-dessus pour optimiser les compilateurs, c'est encore plus directement vrai pour les compilateurs de base, comme mentionné précédemment.

Les Funclet ne concernent pas tant la vitesse qu'ils permettent aux langages avec un flux de contrôle non trivial de se compiler en WebAssembly [..] Relooper est beaucoup plus sujet aux erreurs que d'émettre des funclets

Je suis d'accord à 100% sur le côté des outils. Il est plus difficile d'émettre du code structuré à partir de la plupart des compilateurs ! Mais le fait est que cela simplifie les choses du côté des machines virtuelles, et c'est ce que wasm a choisi de faire. Mais encore une fois, je suis d'accord pour dire que cela comporte des compromis, y compris les inconvénients que vous avez mentionnés.

Wasm s'est-il trompé en 2015? C'est possible. Je pense que nous nous sommes trompés sur certaines choses (comme le débogage et le passage tardif à une machine à pile). Mais il n'est pas possible de les corriger rétrospectivement, et il y a une barre haute pour ajouter de nouvelles choses, en particulier celles qui se chevauchent.

Compte tenu de tout cela, en essayant d'être constructif, je pense que nous devrions résoudre les problèmes existants du côté des outils. Il y a une barre beaucoup, beaucoup plus basse pour les changements d'outils. Deux propositions possibles :

  • Je peux envisager de porter le code Binaryen CFG vers Go, si cela peut aider le compilateur Go - @neelance ?
  • Nous pouvons implémenter des funclets ou quelque chose comme eux uniquement du côté des outils. C'est-à-dire que nous fournissons un code de bibliothèque pour cela aujourd'hui, mais pourrions également ajouter un format binaire. (Il existe déjà un précédent pour l'ajout au format binaire wasm du côté des outils, dans les fichiers objet wasm.)

Nous pouvons implémenter des funclets ou quelque chose comme eux uniquement du côté des outils. C'est-à-dire que nous fournissons un code de bibliothèque pour cela aujourd'hui, mais pourrions également ajouter un format binaire. (Il existe déjà un précédent pour l'ajout au format binaire wasm du côté des outils, dans les fichiers objet wasm.)

S'il y a des travaux concrets à ce sujet, il convient de noter que (AFAIU) le plus petit moyen idiomatique d'ajouter cela à Wasm (comme @rossberg y a fait allusion ) serait d'introduire l'instruction de bloc

multiloop (t in ) _n_ t out (_instr_* end ) _n_

qui définit n corps étiquetés (avec n annotations de type d'entrée déclarées en avant). La famille d'instructions br est ensuite généralisée de sorte que toutes les étiquettes définies par la boucle multiple soient dans la portée de chaque corps, dans l'ordre (comme dans, tout corps peut être ramifié à partir de n'importe quel autre corps). Lorsqu'un corps multiboucle est branché, l'exécution saute au _début_ du corps (comme une boucle Wasm normale). Lorsque l'exécution atteint la fin d'un corps sans se ramifier vers un autre corps, l'ensemble de la construction revient (pas de chute).

Il y aurait un peu de bikeshedding à faire sur la façon de représenter efficacement les annotations de type de chaque corps (dans la formulation ci-dessus, n corps peuvent avoir n types d'entrée différents, mais doivent tous avoir le même type de sortie, donc je ne peux pas utiliser directement des indices _blocktype_ réguliers à plusieurs valeurs sans nécessiter un calcul LUB superflu), et comment sélectionner le corps initial à exécuter (toujours le premier, ou doit-il y avoir un paramètre statique ?).

Cela permet d'obtenir le même niveau d'expressivité que les funclets mais évite d'avoir à introduire un nouvel espace d'instructions de contrôle. En fait, si les funclets avaient été itérés davantage, je pense que cela se serait transformé en quelque chose comme ça.

EDIT: peaufiner cela pour avoir un comportement de secours compliquerait légèrement la sémantique formelle, mais serait probablement mieux pour le cas d'utilisation de

Le principe de conception Wasm consistant à décharger le travail sur les outils pour rendre les moteurs plus simples/plus rapides est très important et continuera d'être très bénéfique.

Cela dit, comme tout ce qui n'est pas trivial, c'est un compromis, pas noir et blanc. Je crois que nous avons ici un cas où la douleur pour les producteurs est disproportionnée par rapport à la douleur pour les moteurs. La plupart des compilateurs que nous aimerions apporter à Wasm utilisent des structures CFG arbitraires en interne (SSA) ou sont utilisés pour cibler des choses qui ne dérangent pas les gotos (CPU). Nous faisons sauter le monde à travers des cerceaux pour pas beaucoup de gain.

Quelque chose comme les funclets (ou multiloop) est sympa car il est modulaire : si un producteur n'en a pas besoin, alors les choses fonctionneront comme avant. Si un moteur ne peut vraiment pas gérer des CFG arbitraires, il peut pour le moment l'émettre comme s'il s'agissait d'un type de construction loop + br_table , et seuls ceux qui l'utilisent en paient le prix . Ensuite, "le marché décide" et nous voyons s'il y a une pression sur les moteurs pour qu'ils émettent un meilleur code pour cela. Quelque chose me dit que s'il doit y avoir beaucoup de code Wasm qui repose sur des funclets, ce ne sera pas vraiment un désastre pour les moteurs d'émettre du bon code pour eux comme certains semblent le penser.

Vous pouvez, mais ce n'est pas anodin - les machines virtuelles devraient le vérifier. Je ne pense pas que cela soit possible en un seul passage linéaire, ce qui serait un problème pour les compilateurs de base, qui sont désormais présents dans la plupart des machines virtuelles.

Peut-être que je comprends mal les attentes d'un compilateur de base, mais pourquoi s'en soucieraient-ils ? Si vous voyez un goto, insérez une instruction de saut.

Je suis d'accord à 100% sur le côté des outils. Il est plus difficile d'émettre du code structuré à partir de la plupart des compilateurs ! Mais le fait est que cela simplifie les choses du côté des machines virtuelles, et c'est ce que wasm a choisi de faire. Mais encore une fois, je suis d'accord pour dire que cela comporte des compromis, y compris les inconvénients que vous avez mentionnés.

Non, comme je le dis plusieurs fois dans mon commentaire d'origine, cela _ne_ facilite pas les choses du côté de la VM. J'ai travaillé sur un compilateur de base pendant plus d'un an et ma vie est devenue plus facile et le code émis est devenu plus rapide après avoir ajouté une étape intermédiaire qui a converti le flux de contrôle de Wasm en un CFG.

Vous pouvez, mais ce n'est pas anodin - les machines virtuelles devraient le vérifier. Je ne pense pas que cela soit possible en un seul passage linéaire, ce qui serait un problème pour les compilateurs de base, qui sont désormais présents dans la plupart des machines virtuelles. (En fait, le simple fait de trouver des backedges de boucle - ce qui est un problème plus simple, et nécessaire pour d'autres raisons également - ne peut pas être fait en une seule passe avant, n'est-ce pas ?)

Ok, voilà le truc, ma connaissance des algorithmes utilisés dans les compilateurs n'est pas assez solide pour affirmer avec une certitude absolue qu'un flux de contrôle irréductible peut ou ne peut pas être détecté dans un compilateur de streaming, mais le fait est qu'il n'a pas besoin de l'être. La vérification peut se produire en tandem avec la compilation. Si un algorithme de streaming n'existe pas, ce que ni vous ni moi ne savons pas, vous pouvez utiliser un algorithme sans streaming une fois que la fonction a été entièrement reçue. Si (pour une raison quelconque) un flux de contrôle irréductible conduit à quelque chose de vraiment mauvais comme une boucle infinie, vous pouvez simplement expirer la compilation et/ou annuler le thread de compilation. Cependant, il n'y a aucune raison de croire que ce serait le cas.

Peut-être que je comprends mal les attentes d'un compilateur de base, mais pourquoi s'en soucieraient-ils ? Si vous voyez un goto, insérez une instruction de saut.

Ce n'est pas si simple à cause de la façon dont vous devez mapper la machine à registres infinis de Wasm (non, ce n'est pas une machine à piles ) aux registres finis du matériel physique, mais c'est un problème que tout compilateur de streaming doit résoudre et c'est entièrement orthogonal à CFG vs flux de contrôle hiérarchique.

Le compilateur de streaming sur lequel j'ai travaillé peut très bien compiler un CFG arbitraire, voire irréductible. Il ne fait rien de particulièrement spécial. Vous attribuez simplement à chaque bloc une "convention d'appel" (essentiellement l'endroit où les valeurs dans la portée de ce bloc devraient être) lorsque vous devez d'abord y accéder, et si jamais vous arrivez à un point où vous devez vous brancher conditionnellement à deux ou plusieurs cibles avec des "conventions d'appel" incompatibles, vous poussez un bloc "adaptateur" dans une file d'attente et l'émettez au prochain point possible. Cela peut se produire à la fois avec un flux de contrôle réductible et irréductible, et ce n'est presque jamais nécessaire dans les deux cas. L'argument du "flux de contrôle irréductible considéré comme nuisible", comme je l'ai déjà dit, est un sujet de discussion et non un argument technique. Représenter le flux de contrôle comme un CFG rend les compilateurs de streaming beaucoup plus faciles à écrire, et comme je l'ai dit à plusieurs reprises, je le sais grâce à une vaste expérience personnelle.

Tous les cas dans lesquels un flux de contrôle irréductible rend les implémentations plus difficiles à écrire, auxquelles je ne peux penser à aucun, peuvent simplement être supprimés et renvoyer une erreur, et si vous avez besoin d'un algorithme distinct, sans flux pour détecter à 100% ce contrôle le flux est irréductible (donc vous n'acceptez pas accidentellement un flux de contrôle irréductible) alors cela peut s'exécuter séparément du compilateur de base lui-même. Quelqu'un que j'ai des raisons de croire est une autorité en la matière (bien que j'éviterai de les invoquer car je sais qu'ils ne veulent pas être entraînés dans ce fil) qu'il existe un algorithme de streaming relativement simple pour détecter l'irréductibilité d'un CFG, mais je ne peux pas dire de première main que cela est vrai.

@oridb

Peut-être que je comprends mal les attentes d'un compilateur de base, mais pourquoi s'en soucieraient-ils ? Si vous voyez un goto, insérez une instruction de saut.

Les compilateurs de base doivent toujours faire des choses comme insérer des vérifications supplémentaires au niveau des backedges de boucle (c'est ainsi qu'une page suspendue sur le Web affichera éventuellement un dialogue de script lent), ils doivent donc identifier des choses comme ça. De plus, ils essaient de faire une allocation de registres raisonnablement efficace (les compilateurs de base s'exécutent souvent à environ 1/2 de la vitesse du compilateur d'optimisation - ce qui est très impressionnant étant donné qu'ils sont en un seul passage !). Avoir la structure du flux de contrôle, y compris les jointures et les scissions, rend cela beaucoup plus facile.

@gwvo

Cela dit, comme tout ce qui n'est pas trivial, c'est un compromis, pas noir et blanc. [..] Nous faisons sauter le monde à travers des cerceaux pour pas beaucoup de gain.

Tout à fait d'accord, c'est un compromis, et même peut-être que je me suis trompé à l'époque. Mais je pense qu'il est beaucoup plus pratique de réparer ces cerceaux du côté des outils.

Ensuite, "le marché décide" et nous voyons s'il y a une pression sur les moteurs pour qu'ils émettent un meilleur code pour cela.

C'est en fait quelque chose que nous avons évité jusqu'à présent. Nous avons essayé de rendre wasm aussi simple que possible sur la VM afin qu'il ne nécessite pas d'optimisations complexes - pas même des choses comme l'inline, autant que possible. L'objectif est de faire le gros du travail du côté des outils, pas de faire pression sur les VM pour qu'elles fassent mieux.

@Vurich

J'ai travaillé sur un compilateur de base pendant plus d'un an et ma vie est devenue plus facile et le code émis est devenu plus rapide après avoir ajouté une étape intermédiaire qui a converti le flux de contrôle de Wasm en un CFG.

Très intéressant! C'était quelle VM ?

Je serais également particulièrement curieux de savoir s'il s'agissait d'un seul passage/diffusion en continu ou non (si c'était le cas, comment a-t-il géré l'instrumentation de backedge en boucle?), Et comment il enregistre l'allocation.

En principe, les backedges de boucle et l'allocation de registres peuvent être gérés sur la base d'un ordre d'instruction linéaire, dans l' attente que les blocs de base seront placés dans un ordre de type topsort raisonnable, sans l'exiger strictement.

Pour les backedges de boucle : définissez un backedge comme une instruction qui saute plus tôt dans le flux d'instructions. Au pire, si les blocs sont disposés à l'envers, vous obtenez plus de contrôles de backedge que strictement nécessaires.

Pour l'allocation de registre : il s'agit simplement d' une allocation de registre de balayage linéaire standard. La durée de vie d'une variable pour l'allocation de registres s'étend de la première mention de la variable à la dernière mention, y compris tous les blocs qui sont linéairement entre les deux. Au pire, si les blocs sont mélangés, vous obtenez des durées de vie plus longues que nécessaire et ainsi renversez inutilement des choses sur la pile. Le seul coût supplémentaire est le suivi de la première et de la dernière mention de chaque variable, ce qui peut être fait pour toutes les variables avec un seul balayage linéaire. (Pour wasm, je suppose qu'une "variable" est soit un emplacement local, soit un emplacement de pile.)

@kripken

Je peux envisager de porter le code Binaryen CFG vers Go, si cela peut aider le compilateur Go - @neelance ?

Pour intégrer Asyncify ? Veuillez commenter la proposition .

@comex

Bons points!

Le seul coût supplémentaire est le suivi de la première et de la dernière mention de chaque variable

Oui, je pense que c'est une différence significative. L'allocation de registre d'analyse linéaire est meilleure (mais plus lente à faire) que ce que font actuellement les compilateurs de base de wasm , car ils compilent en flux continu, ce qui est très rapide. C'est-à-dire qu'il n'y a pas d'étape initiale pour trouver la dernière mention de chaque variable - ils compilent en un seul passage, émettant du code au fur et à mesure sans même voir le code plus tard dans la fonction wasm, aidés par la structure, et ils simplifient également choix au fur et à mesure ("stupide" est le mot utilisé dans ce post).

L'approche de streaming de V8 pour enregistrer l'allocation devrait tout aussi bien fonctionner si les blocs sont autorisés à être mutuellement récursifs (comme dans https://github.com/WebAssembly/design/issues/796#issuecomment-742690194), puisque les seules durées de vie qu'ils traitent sont liés dans un seul bloc (pile) ou supposés être à l'échelle de la fonction (local).

IIUC (en référence à l » @titzer commentaire ) , le problème principal réside V8 dans le genre de CFG que turbosoufflante peut optimiser.

@kripken

Nous avons essayé de rendre wasm aussi simple que possible sur la VM afin qu'il ne nécessite pas d'optimisations complexes

Ce n'est pas une "optimisation complexe". Les gotos sont incroyablement basiques et naturels pour de nombreux systèmes. Je parie qu'il y a beaucoup de moteurs qui pourraient ajouter cela sans frais. Tout ce que je dis, c'est que s'il y a des moteurs qui veulent conserver un modèle CFG structuré pour une raison quelconque, ils le peuvent.

Par exemple, je suis à peu près sûr que LLVM (de loin notre premier producteur Wasm actuellement) ne passera pas à l'utilisation de funclets tant qu'il n'aura pas la certitude qu'il ne s'agit pas d'une régression des performances dans les principaux moteurs.

@kripken Cela fait partie de Wasmtime. Oui, c'est du streaming et était censé être une complexité O(N), mais j'ai déménagé dans une nouvelle entreprise avant que cela ne soit pleinement réalisé, donc ce n'est que "O(N)-ish". https://github.com/bytecodealliance/wasmtime/tree/main/crates/lightbeam

Merci @Vurich , intéressant. Ce serait formidable de voir les chiffres de perf lorsque ceux-ci sont disponibles, en particulier pour le démarrage mais aussi pour le débit. Je suppose que votre approche compilerait plus lentement que l'approche adoptée par les ingénieurs de V8 et SpiderMonkey, tout en émettant un code plus rapide. C'est donc un compromis différent dans cet espace. Il semble plausible que votre approche ne bénéficie pas du flux de contrôle structuré de wasm, comme vous l'avez dit, contrairement à la leur.

Non, c'est un compilateur de streaming et émet du code plus rapidement que l'un ou l'autre de ces deux moteurs (bien qu'il existe des cas dégénérés qui n'étaient pas résolus au moment où j'ai quitté le projet). Bien que j'aie fait de mon mieux pour émettre du code rapide, il est principalement conçu pour émettre du code rapidement, l'efficacité de la sortie étant une préoccupation secondaire. Le coût de démarrage est, à ma connaissance, nul (au-dessus du coût inhérent de Wasmtime qui est partagé entre les backends) car chaque structure de données démarre non initialisée et la compilation se fait instruction par instruction. Bien que je n'aie pas de chiffres à comparer à V8 ou SpiderMonkey à portée de main, j'ai des chiffres à comparer à Cranelift (le moteur principal en temps de travail). Ils sont obsolètes à ce stade de plusieurs mois, mais vous pouvez voir que non seulement il émet du code plus rapidement que Cranelift, mais qu'il émet également un code plus rapide que Cranelift. À l'époque, il émettait également un code plus rapide que SpiderMonkey, bien que vous deviez me croire sur parole, donc je ne vous blâmerai pas si vous ne me croyez pas. Bien que je n'aie pas de chiffres plus récents à portée de main, je pense que l'état actuel est que Cranelift et SpiderMonkey ont tous deux corrigé la petite poignée de bugs qui étaient la principale source de leur faible rendement dans ces microbenchmarks par rapport à Lightbeam, mais le différentiel de vitesse de compilation n'a pas changé tout le temps que j'étais sur le projet car chaque compilateur est toujours fondamentalement architecturé de la même manière, et c'est l'architecture respective qui conduit aux différents niveaux de performances. Bien que j'apprécie vos spéculations, je ne sais pas d'où vient votre hypothèse selon laquelle la méthode que j'ai décrite serait plus lente.

Voici les repères, les repères ::compile sont pour la vitesse de compilation et les repères ::run sont pour la vitesse d'exécution de la sortie du code machine. https://gist.github.com/Vurich/8696e67180aa3c93b4548fb1f298c29e

La méthodologie est là, vous pouvez la cloner et réexécuter les benchmarks pour confirmer les résultats par vous-même, mais le PR sera probablement incompatible avec la dernière version de wasmtime, il ne vous montrera donc que la comparaison des performances au moment de la dernière mise à jour du RP. https://github.com/bytecodealliance/wasmtime/pull/1660

Cela étant dit, mon argument n'est _pas_ que les CFG sont une représentation interne utile pour les performances dans un compilateur de streaming. Mon argument est que les CFG n'affectent négativement les performances dans aucun compilateur, et certainement pas au niveau qui justifierait d'interdire complètement aux équipes GCC et Go de produire WebAssembly. Presque personne dans ce fil de discussion contre les funclets ou une extension similaire de wasm n'a réellement travaillé sur les projets qui, selon eux, seront affectés négativement par cette proposition. Cela ne veut pas dire que vous avez besoin d'une expérience de première main pour commenter ce sujet, je pense que tout le monde a un certain niveau de contribution précieuse, mais cela veut dire qu'il y a une ligne entre avoir une opinion différente sur la couleur du bikeshed et faire réclamations basées sur rien de plus que de vaines spéculations.

@Vurich

Non, c'est un compilateur de streaming et émet du code plus rapidement que l'un ou l'autre de ces deux moteurs (bien qu'il y ait des cas dégénérés qui n'ont jamais été corrigés parce que j'ai quitté le projet).

Désolé si je n'ai pas été assez clair plus tôt. Pour être sûr que nous parlons de la même chose, je voulais dire les compilateurs de base dans ces moteurs. Et je parle du temps de compilation, qui est le but des compilateurs de base au sens où V8 et SpiderMonkey utilisent le terme.

La raison pour laquelle je suis sceptique que vous puissiez battre les temps de compilation de base de V8 et SpiderMonkey est que, comme dans les liens que j'ai donnés plus tôt, ces deux compilateurs de base sont extraordinairement réglés pour le temps de compilation. En particulier, ils ne génèrent aucun IR interne, ils passent juste du code wasm au code machine. Vous avez dit que votre compilateur émet un IR interne (pour un CFG) - je m'attendrais à ce que vos temps de compilation soient plus lents juste à cause de cela (en raison de plus de branchements, de bande passante mémoire, etc.).

Mais s'il vous plaît, comparez-vous à ces compilateurs de base ! J'aimerais voir des données montrant que ma supposition est fausse, et je suis sûr que les ingénieurs V8 et SpiderMonkey le feraient aussi. Cela signifierait que vous avez trouvé un meilleur design qu'ils devraient envisager d'adopter.

Pour tester contre V8, vous pouvez exécuter d8 --liftoff --no-wasm-tier-up , et pour SpiderMonkey, vous pouvez exécuter sm --wasm-compiler=baseline .

(Merci pour les instructions de comparaison avec Cranelift, mais Cranelift n'est pas un compilateur de base, donc comparer les temps de compilation avec lui n'est pas pertinent dans ce contexte. Très intéressant sinon, je suis d'accord.)

Mon intuition est que les compilateurs de base n'auraient pas à modifier de manière significative leur stratégie de compilation pour prendre en charge les funclets/ multiloop , car ils n'essaient de toute façon pas de faire une optimisation inter-bloc significative. La "structure du flux de contrôle, y compris les jointures et les séparations " référencée par @kripken est satisfaite en exigeant que tous les types d'entrée pour une collection de blocs mutuellement récursifs soient déclarés (ce qui semble de toute façon le choix naturel pour la validation en continu) . Que Lightbeam/Wasmtime puisse battre les compilateurs de base de moteur n'entre pas en ligne de compte ; le point important est de savoir si les compilateurs de base de moteur peuvent rester aussi rapides qu'ils le sont maintenant.

FWIW, je serais intéressé de voir cette fonctionnalité abordée lors d'une future réunion CG, et je suis globalement d'accord avec @Vurich que les représentants du moteur peuvent s'opposer eux-mêmes s'ils ne sont pas prêts à la mettre en œuvre. Cela étant dit, nous devrions prendre toute objection de ce type au sérieux (j'ai déjà déclaré lors de réunions en personne qu'en poursuivant cette fonctionnalité, nous devrions essayer d'éviter une version WebAssembly de la saga JavaScript

@kripken

Oui, je pense que c'est une différence significative. L'allocation de registre d'analyse linéaire est meilleure (mais plus lente à faire) que ce que font actuellement les compilateurs de base de wasm , car ils compilent en flux continu, ce qui est très rapide. C'est-à-dire qu'il n'y a pas d'étape initiale pour trouver la dernière mention de chaque variable - ils compilent en un seul passage, émettant du code au fur et à mesure sans même voir le code plus tard dans la fonction wasm, aidés par la structure, et ils simplifient également choix au fur et à mesure ("stupide" est le mot utilisé dans ce post).

Wow, c'est vraiment très simple.

D'un autre côté… cet algorithme particulier est si simple qu'il ne dépend d'aucune propriété profonde du flux de contrôle structuré. Cela dépend à peine des propriétés superficielles du flux de contrôle structuré.

Comme le mentionne l'article de blog, le compilateur de base de Wasm de SpiderMonkey ne préserve pas l'état de l'allocateur de registre via des "jointures de flux de contrôle" (c'est-à-dire des blocs de base avec plusieurs prédécesseurs), à la place d'une ABI fixe ou d'un mappage de la pile wasm vers la pile et les registres natifs. . J'ai découvert en testant qu'il utilise également une ABI fixe lors de la saisie de blocs , même s'il ne s'agit pas d'une jointure de flux de contrôle dans la plupart des cas !

L'ABI fixe est la suivante (sur x86) :

  • S'il y a un nombre non nul de paramètres (lors de l'entrée dans un bloc) ou de retours (lors de la sortie d'un bloc), alors le haut de la pile wasm va dans rax , et le reste de la pile wasm correspond au x86 empiler.
  • Sinon, toute la pile wasm correspond à la pile x86.

Pourquoi est-ce important ?

Parce que cet algorithme pourrait fonctionner presque de la même manière avec beaucoup moins d'informations. À titre d'expérience de réflexion, imaginez une version d'univers alternatif de WebAssembly où il n'y avait pas d'instructions de flux de contrôle structurées, juste des instructions de saut, similaires à l'assemblage natif. Il faudrait l'enrichir d'une seule information supplémentaire : un moyen de savoir quelles instructions sont les cibles des sauts.

Alors l'algorithme serait simplement : parcourir les instructions de manière linéaire ; avant les sauts et les cibles de saut, vider les registres vers l'ABI fixe.

La seule différence est qu'il devrait y avoir un seul ABI fixe, pas deux. Il ne pouvait pas faire la distinction entre la valeur du sommet de la pile étant sémantiquement le « résultat » d'un saut, et le simple fait d'être laissé sur la pile à partir d'un bloc extérieur. Il faudrait donc mettre inconditionnellement le sommet de la pile dans rax .

Mais je doute que cela ait un coût mesurable pour la performance ; si quoi que ce soit, cela pourrait être une amélioration.

(La vérification serait également différente mais toujours en un seul passage.)

D'accord, mises en garde initiales :

  1. Ce n'est pas un univers alternatif ; nous sommes obligés de créer des extensions rétrocompatibles pour le WebAssembly existant.
  2. Le compilateur de base de SpiderMonkey n'est qu'une implémentation, et il est possible qu'il soit sous-optimal en ce qui concerne l'allocation des registres : s'il était un peu plus intelligent, les avantages de l'exécution l'emporteraient sur le coût de la compilation.
  3. Même si les compilateurs de base n'ont pas besoin d'informations supplémentaires, les compilateurs d'optimisation peuvent en avoir besoin pour une construction SSA rapide.

Avec ceux-ci à l'esprit, l'expérience de pensée ci-dessus renforce ma conviction que les compilateurs de base n'ont pas besoin d'un flux de contrôle structuré . Quel que soit le niveau bas d'une construction que nous ajoutons, tant qu'elle inclut des informations de base telles que les instructions qui sont des cibles de saut, les compilateurs de base peuvent la gérer avec seulement des modifications mineures. Ou du moins celui-ci peut.

@conrad-watt @comex

Ce sont de très bons points ! Mon intuition sur les compilateurs de base peut alors être fausse.

Et @comex - oui, comme vous l'avez dit, cette discussion est distincte de l'optimisation des compilateurs où SSA peut bénéficier de la structure. Cela vaut peut-être la peine de citer un peu l'un des liens précédents :

De par sa conception, la transformation du code WebAssembly en IR de TurboFan (y compris la construction SSA) en un seul passage est très efficace, en partie grâce au flux de contrôle structuré de WebAssembly.

@conrad-watt Je suis tout à fait d'accord que nous avons juste besoin d'obtenir des commentaires directs des personnes VM et de garder l'esprit ouvert. Pour être clair, mon but ici n'est pas d'arrêter quoi que ce soit. J'ai longuement commenté ici parce que plusieurs commentaires semblaient penser que le flux de contrôle structuré de wasm était une erreur évidente ou qu'il fallait évidemment y remédier avec des funclets/multiloop - je voulais juste présenter l'historique de la pensée ici, et qu'il y avait de fortes raisons pour le modèle actuel, il n'est donc peut-être pas facile de l'améliorer.

J'ai beaucoup aimé lire cette conversation. Je me suis moi-même posé un tas de ces questions (venant des deux directions) et j'ai partagé bon nombre de ces réflexions (encore une fois dans les deux directions), et la discussion a offert beaucoup d'idées et d'expériences utiles. Je ne suis pas sûr d'avoir encore une opinion bien arrêtée, mais j'ai une pensée à apporter dans chaque direction.

Du côté "pour", il est utile de savoir à l'avance quels blocs ont des backedges. Un compilateur de streaming peut suivre les propriétés qui ne sont pas apparentes dans le système de types de WebAssembly (par exemple, l'index en local i est dans les limites du tableau en local arr ). Lorsque vous sautez en avant, il peut être utile d'annoter la cible avec les propriétés qui s'y trouvent. De cette façon, lorsqu'une étiquette est atteinte, son bloc peut être compilé à l'aide des propriétés qui s'appliquent à tous les bords internes, par exemple pour éliminer les vérifications des limites du tableau. Mais si une étiquette peut potentiellement avoir un backedge inconnu, alors son bloc ne peut pas être compilé avec cette connaissance. Bien sûr, un compilateur sans streaming peut effectuer des analyses invariantes de boucle plus importantes, mais pour un compilateur en streaming, il est utile de ne pas avoir à se soucier de ce qui pourrait arriver. (Pensée latérale : @Vurich mentionne que WebAssembly n'est pas une machine à pile en raison de son utilisation de locaux. Dans #1381, j'ai exposé quelques raisons de moins dépendre des locaux et d'ajouter plus d'opérations de pile. cette direction.)

Du côté "contre", jusqu'à présent, la discussion s'est concentrée uniquement sur le contrôle local. C'est bien pour C, mais qu'en est-il pour C++ ou divers autres langages avec des exceptions similaires ? Qu'en est-il des langues avec d'autres formes de contrôle non local ? Les choses avec une portée dynamique sont souvent intrinsèquement structurées (ou du moins, je ne connais aucun exemple de portée dynamique mutuellement récursive). Je pense que ces considérations sont adressables, mais vous devez concevoir quelque chose en pensant à elles pour que le résultat soit utilisable dans ces paramètres. C'est quelque chose que j'ai réfléchi, et je suis heureux de partager mes réflexions en cours (ressemblant à peu près à une extension de la multi-boucle de @conrad-watt) avec toute personne intéressée (bien que cela semble hors sujet), mais Je voulais au moins prévenir qu'il y a plus qu'un simple flux de contrôle local à garder à l'esprit.

(J'aimerais également ajouter un autre +1 pour en savoir plus sur les personnes VM, même si je pense que

Quand je dis que Lightbeam produit un IR interne, c'est vraiment trompeur et j'aurais dû clarifier. J'ai travaillé sur le projet pendant un certain temps et parfois, vous pouvez avoir une vision en tunnel. Fondamentalement, Lightbeam consomme l'instruction d'entrée par instruction (il a en fait un maximum d'une instruction anticipée mais ce n'est pas particulièrement important), et pour chaque instruction, il produit, paresseusement et dans un espace constant, un certain nombre d'instructions IR internes. Le nombre maximum d'instructions par instruction Wasm est constant et petit, quelque chose comme 6. Il ne s'agit pas de créer un tampon d'instructions IR pour l'ensemble de la fonction et de travailler dessus. Ensuite, il lit ces instructions IR une par une. Vous pouvez vraiment le considérer comme une bibliothèque de fonctions d'assistance plus génériques qui implémentent chaque instruction Wasm en termes de, je l'appelle simplement IR car cela aide à expliquer comment il a un modèle différent pour le flux de contrôle, etc. Il ne produit probablement pas de code aussi rapidement que les compilateurs de base de V8 ou de SpiderMonkey, mais c'est parce qu'il n'est pas entièrement optimisé et non parce qu'il est déficient sur le plan architectural. Mon point est que je modélise en interne le flux de contrôle hiérarchique de Wasm comme s'il s'agissait d'un CFG, plutôt que de produire réellement un tampon d'IR en mémoire comme le font LLVM ou Cranelift.

Une autre option consiste à compiler le wasm en quelque chose qui, selon vous, peut gérer le flux de contrôle de manière optimale, c'est-à-dire "annuler" la structuration. LLVM devrait pouvoir le faire, donc exécuter wasm dans une VM qui utilise LLVM (comme WAVM ou wasmer) ou via WasmBoxC pourrait être intéressant.

@kripken Malheureusement, LLVM ne semble pas encore capable d'annuler la structuration. La passe d'optimisation des threads de saut devrait être capable de le faire, mais ne reconnaît pas encore ce modèle. Voici un exemple montrant du code C++ qui imite comment l'algorithme relooper convertirait un CFG en boucle+commutateur. GCC parvient à le "dereloop", mais clang ne le fait pas : https://godbolt.org/z/GGM9rP

@AndrewScheidecker Intéressant, merci. Oui, ce truc peut être assez imprévisible, il n'y a donc peut-être pas de meilleure option que d'étudier le code émis (comme le fait l'article "No So Fast" lié plus tôt), et d'éviter les tentatives de raccourcis comme s'appuyer sur l'optimiseur de LLVM.

@comex

Le compilateur de base de SpiderMonkey n'est qu'une implémentation, et il est possible qu'il soit sous-optimal en ce qui concerne l'allocation des registres : s'il était un peu plus intelligent, les avantages de l'exécution l'emporteraient sur le coût de la compilation.

Il pourrait clairement être plus intelligent sur l'attribution des registres. Il se répand indistinctement lors des fourches de flux de contrôle, des jointures et avant les appels, et pourrait conserver plus d'informations sur l'état du registre et essayer de conserver les valeurs dans les registres plus longtemps / jusqu'à ce qu'elles soient mortes. Il pourrait choisir un meilleur registre que rax pour les résultats de valeur des blocs, ou mieux, ne pas utiliser un registre fixe. Il pourrait dédier statiquement quelques registres pour contenir des variables locales ; une analyse de corpus que j'ai faite a suggéré que quelques registres entiers et FP seraient suffisants pour la plupart des fonctions. Il pourrait être plus intelligent de renverser en général ; en l'état, il panique tout lorsqu'il n'a plus de registres.

Le coût en temps de compilation de ceci est principalement que chaque bord de flux de contrôle aura une quantité non constante d'informations qui lui est associée (l'état du registre) et cela peut conduire à une utilisation plus répandue de l'allocation de stockage dynamique, ce que le compilateur de base a donc de loin évité. Et bien sûr, il y aura un coût associé au traitement de ces informations de taille variable à chaque jointure (et à d'autres endroits). Mais il y a déjà un coût non constant puisque l'état du registre doit être traversé pour générer du code de débordement, et dans l'ensemble, il peut y avoir peu de valeurs en direct, donc cela peut être OK (ou pas). Bien sûr, être plus intelligent avec le regalloc peut ou non être payant sur les puces modernes, avec leurs caches rapides et leur exécution ooo...

Un coût plus subtil est la maintenabilité du compilateur... c'est déjà assez complexe, et puisqu'il s'agit d'un seul passage et qu'il ne construit pas de graphique IR ou n'utilise pas du tout de mémoire dynamique, il est résistant à la superposition et à l'abstraction.

@RossTate

Re funclets / gotos, j'ai parcouru la spécification funclet l'autre jour et à première vue, il ne semblait pas qu'un compilateur en un seul passage devrait avoir de réels problèmes avec cela, certainement pas avec un schéma regalloc simpliste. Mais même avec un meilleur schéma, cela pourrait être OK : le premier bord à atteindre un point de jointure pourrait décider quelle est l'affectation du registre, et les autres bords devraient se conformer.

@conrad-watt comme vous venez de le mentionner lors de la réunion CG, je pense que nous serions très intéressés de voir des détails sur ce à quoi ressemblerait votre multi-boucle.

@aardappel oui, la vie m'est venue rapidement, mais je devrais le faire lors de la prochaine réunion. Juste pour souligner que l'idée n'est pas la mienne puisque @rossberg l'a initialement esquissé en réponse au premier brouillon de funclets.

Une référence qui pourrait être instructive est un peu datée, mais généralise les notions familières de boucles pour gérer les irréductibles à l'aide de graphes DJ .

Nous avons eu quelques séances de discussion à ce sujet au sein du CG, et j'ai rédigé un document de synthèse et de suivi. En raison de la longueur, j'en ai fait un élément essentiel.

https://gist.github.com/conrad-watt/6a620cb8b7d8f0191296e3eb24dffdef

Je pense que les deux questions immédiatement exploitables (voir la section de suivi pour plus de détails) sont :

  • Peut-on trouver des programmes "sauvages" qui souffrent actuellement et bénéficieraient en termes de performances de multiloop ? Il peut s'agir de programmes pour lesquels les transformations LLVM introduisent un flux de contrôle irréductible même s'il n'existe pas dans le programme source.
  • Existe-t-il un monde où multiloop est d'abord implémenté côté producteur, avec une couche de déploiement de liaison/traduction pour Wasm « Web » ?

Il y a probablement aussi une discussion plus libre sur les conséquences des problèmes de gestion des exceptions dont je parle dans le document de suivi, et bien sûr le bikeshedding standard sur les détails sémantiques si nous avançons avec quelque chose de concret.

Étant donné que ces discussions peuvent quelque peu bifurquer, il peut être approprié de transformer certaines d'entre elles en problèmes dans le référentiel de funclets .

Je suis très heureux de voir des progrès sur cette question. Un grand "Merci" à toutes les personnes impliquées !

Peut-on trouver des programmes "sauvages" qui souffrent actuellement et bénéficieraient en termes de performances du multiloop ? Il peut s'agir de programmes pour lesquels les transformations LLVM introduisent un flux de contrôle irréductible même s'il n'existe pas dans le programme source.

Je voudrais mettre un peu en garde contre le raisonnement circulaire : les programmes qui ont actuellement de mauvaises performances sont moins susceptibles de se produire « à l'état sauvage » pour exactement cette raison.

Je pense que la plupart des programmes de Go devraient en bénéficier grandement. Le compilateur Go a besoin des coroutines WebAssembly ou de multiloop pour pouvoir émettre un code efficace prenant en charge les goroutines de Go.

Les matchers d'expressions régulières précompilés, ainsi que d'autres machines à états précompilées, entraînent souvent un flux de contrôle irréductible. Il est difficile de dire si l'algorithme de "fusion" pour les types d'interface entraînera ou non un flux de contrôle irréductible.

  • D'accord, cette discussion doit être déplacée vers les problèmes sur les funclets (ou un nouveau) repo.
  • Convenez qu'il est difficile de quantifier le programme qui en bénéficierait sans que LLVM (et Go, et d'autres) émettent réellement le flux de contrôle le plus optimal (qui peut être irréductible). L'inefficacité causée par FixIrreducibleControlFlow et les amis peut être un problème de "mort par mille coupures" sur un grand binaire.
  • Même si j'accueillerais une implémentation d'outils uniquement comme progrès minimum absolu résultant de cette discussion, ce ne serait toujours pas optimal, car les producteurs ont maintenant le choix difficile d'utiliser cette fonctionnalité pour plus de commodité (mais sont alors confrontés à des régressions de performances imprévisibles/ falaises), ou faire le travail acharné pour brouiller leur sortie au niveau standard pour que les choses soient prévisibles.
  • S'il était décidé que les "gotos" sont au mieux une fonctionnalité réservée aux outils, je dirais que vous pourriez probablement vous en tirer avec une fonctionnalité encore plus simple que multiloop, car tout ce qui vous intéresse est la commodité du producteur. Au minimum absolu, un goto <function_byte_offset> serait la seule chose à insérer dans les corps de fonction Wasm normaux pour permettre à WABT ou à Binaryen de le transformer en Wasm légal. Des éléments tels que les signatures de type sont utiles si les moteurs ont besoin de vérifier rapidement une boucle multiple, mais s'il s'agit d'un outil pratique, ils pourraient tout aussi bien le rendre plus pratique à émettre.

Convenez qu'il est difficile de quantifier le programme qui en bénéficierait sans que LLVM (et Go, et d'autres) émettent réellement le flux de contrôle le plus optimal (qui peut être irréductible).

Je conviens que des tests sur des chaînes d'outils modifiées + des machines virtuelles seraient optimaux. Mais nous pouvons comparer les builds wasm actuels aux builds natifs qui ont un flux de contrôle optimal. Not So Fast et d'autres ont examiné cela de diverses manières (compteurs de performances, enquête directe) et n'ont pas trouvé que le flux de contrôle irréductible était un facteur important.

Plus précisément, ils n'ont pas trouvé que c'était un facteur significatif pour C/C++. Cela pourrait avoir plus à voir avec C/C++ qu'avec les performances d'un flux de contrôle irréductible. (Honnêtement, je ne sais pas.) On dirait que @neelance a des raisons de croire qu'il n'en va pas de même pour Go.

J'ai l'impression qu'il y a plusieurs facettes à ce problème et qu'il vaut la peine de s'y attaquer dans plusieurs directions.

Premièrement, il semble qu'il y ait un problème général avec la générabilité de WebAssembly. Cela est dû en grande partie à la contrainte de WebAssembly d'avoir un binaire compact avec une vérification de type efficace et une compilation en streaming. Nous pourrions résoudre ce problème au moins en partie en développant un "pré"-WebAssembly standardisé qui est plus facile à générer mais qui est garanti d'être traduisible en "vrai" WebAssembly, idéalement grâce à la duplication de code et l'insertion d'instructions/annotations "effaçables", avec au moins un outil fournissant une telle traduction.

Deuxièmement, nous pouvons considérer quelles fonctionnalités du "pré"-WebAssembly valent la peine d'être directement intégrées dans le "vrai" WebAssembly. Nous pouvons le faire de manière éclairée car nous aurons des modules "pré"-WebAssembly que nous pourrons analyser avant qu'ils n'aient été transformés en "vrais" modules WebAssembly.

Il y a quelques années, j'ai essayé de compiler un émulateur de bytecode particulier pour un langage dynamique (https://github.com/ciao-lang/ciao) en webassembly et les performances étaient loin d'être optimales (parfois 10 fois plus lentes que la version native). La boucle d'exécution principale contenait un grand commutateur de répartition de bytecode, et le moteur a été finement réglé pendant des décennies pour fonctionner sur du matériel réel, et nous utilisons beaucoup d'étiquettes et de gotos. Je me demande si ce genre de logiciel bénéficierait d'un support de flux de contrôle irréductible ou si le problème en était un autre. Je n'ai pas eu le temps de faire une enquête plus approfondie, mais je serais heureux de réessayer si les choses se sont améliorées. Bien sûr, je comprends que la compilation d'autres langages VM vers wasm n'est pas le cas d'utilisation principal, mais je serais bon de savoir si cela sera finalement faisable, d'autant plus que les binaires universels qui s'exécutent efficacement, partout, est l'un des avantages promis de était M. (Merci et excuses si ce sujet particulier a été abordé dans un autre numéro)

@jfmc Je crois comprendre que si le programme est réaliste (c'est-à-dire non conçu pour être pathologique) et que vous vous souciez de ses performances, alors c'est un cas d'utilisation parfaitement valide. WebAssembly se veut une bonne cible polyvalente. Je pense donc que ce serait formidable de comprendre pourquoi vous avez constaté un ralentissement aussi important. Si cela est dû à des restrictions sur le flux de contrôle, il serait alors très utile de le savoir dans cette discussion. Si cela est dû à autre chose, il serait toujours utile de savoir comment améliorer WebAssembly en général.

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

Questions connexes

konsoletyper picture konsoletyper  ·  6Commentaires

spidoche picture spidoche  ·  4Commentaires

nikhedonia picture nikhedonia  ·  7Commentaires

jfbastien picture jfbastien  ·  6Commentaires

badumt55 picture badumt55  ·  8Commentaires