Bidouilleux d'Web - Mot-clé - InterpreteurLe blog technique de la communauté Mozilla francophone2022-02-04T14:07:43+01:00Communauté Mozilla francophoneurn:md5:935a9b6df47b29d6dc8c2ca47296b179DotclearOptimiser les accès de variables JavaScripturn:md5:2157a22f22027cd45de5971747cba8382013-03-14T00:22:00-07:002013-03-14T10:42:43-07:00nbpJavaScriptcompilateurInterpreteurJavaScript <p>Cet article est issu du <a href="https://blog.mozilla.org/luke/" hreflang="en">blog de Luke Wagner</a>. L’article original a été <a href="https://blog.mozilla.org/luke/2012/10/02/optimizing-javascript-variable-access/" hreflang="en">écrit en anglais</a> par Luke Wagner.</p>
<p>J’ai récemment fini <a href="https://bugzilla.mozilla.org/showdependencygraph.cgi?id=767013" hreflang="en">un projet</a> visant à améliorer la manière dont SpiderMonkey implémente les accès de variable, ainsi j’ai pensé qu’il était temps d’expliquer comment cela fonctionne désormais. En m’inspirant du <a href="http://blog.mrale.ph/post/24351748336/explaining-js-vm-in-js" hreflang="en" title="billet de mraleph">billet de mraleph</a> ainsi que du livre <em><a href="http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-25.html#%_chap_4" hreflang="fr" title="référence au livre Structure et interprétation de programmes">Structure et Interprétation de Programmes</a></em>, je vais illustrer l’implémentation en utilisant JavaScript comme langage d’implémentation. Par conséquent, je vais traduire du JavaScript qui utilise des accès de variable complexe en du JavaScript qui n’en utilise pas, un peu comme les premiers compilateurs <a href="http://en.wikipedia.org/wiki/Cfront" hreflang="en">C++ traduisaient le C++</a> vers du C.</p>
<p>Avant de commencer, laissez-moi vous montrer l’étendue du problème. Par variable, je me réfère non seulement aux variables introduites par var, mais aussi à celles introduites par <a href="http://wiki.ecmascript.org/doku.php?id=harmony:block_scoped_bindings" hreflang="en">let</a>, <a href="http://wiki.ecmascript.org/doku.php?id=harmony:const" hreflang="en">const</a>, catch, les déclarations de fonction, et les arguments des fonctions. Par accès de variable, je veux dire une lecture ou une écriture. Un accès de variable peut prendre l’une des formes suivantes :</p>
<ul>
<li>Un accès local (c.-à-d. un accès à une variable dans la même fonction) :</li>
</ul>
<pre>function add(x,y) { return x+y }</pre>
<ul>
<li>Un accès non local (c.-à-d. un accès à une variable d’une fonction englobante) :</li>
</ul>
<pre>function add(x,y) { return (function() { return x+y })() }</pre>
<ul>
<li>Un accès depuis du code généré dynamiquement :</li>
</ul>
<pre>function add(x,y) { return eval("x+y") }</pre>
<ul>
<li>Un accès après une modification dynamique de portée via un eval direct non strict :</li>
</ul>
<pre>function add(a,b) { eval("var x="+a+", y="+b); return x+y }</pre>
<ul>
<li>Un accès dynamique aux arguments de la fonction via l’objet arguments :</li>
</ul>
<p><pre>function add(x,y) { return arguments[0]+arguments[1] }</pre></p>
<ul>
<li>Une inspection inattendue par un débogueur (via <a href="http://getfirebug.com/" hreflang="en">Firebug</a>, la nouvelle <a href="https://developer.mozilla.org/en-US/docs/Tools/Debugger" hreflang="en">primitive de débogage de Firefox</a>, ou directement depuis du JavaScript privilégié utilisant la <a href="https://wiki.mozilla.org/Debugger" hreflang="en">nouvelle interface Debugger</a>) :</li>
</ul>
<pre>dbg.onDebugerStatement = function(f) { return f.eval("x+y") }</pre>
<p>Afin de ne pas allonger ce petit billet de façon démesurée, je vais prétendre/supposer qu’il y a seulement des evals (non strict, direct) et ignorer les evals stricts et indirects ainsi que le mot-clé with (qui généralement désoptimise comme si c’était un eval). Je vais aussi ignorer les let et les optimisations des accès de variables globales, ainsi que les bizarreries que SpiderMonkey fait pour la portée des fonctions et pour le débogueur.</p>
<h2>Le pire des cas</h2>
<p>Pour s’en sortir, nous devons avant tout voir jusqu’à quel niveau de détail on doit aller dans le pire des cas. Considérons la fonction suivante :</p>
<pre>function bizarre() {
eval("var x = 42");
return function xPlus1() { var z = x + 1; return z }
}</pre>
<p>Ici, eval ajoute dynamiquement x à la portée de la fonction bizarre où il sera lu par xPlus1. Vu que eval peut aussi être appelé avec une chaîne construite dynamiquement, on doit, en général, traiter la portée d’une fonction comme un tableau associatif de noms de variables vers les valeurs de chacune. (Fait intéressant, les noms de variables rajoutés avec eval peuvent être supprimés en utilisant le mot clé delete, ainsi ce tableau associatif peut grossir et se réduire lors de l’exécution !) Afin de rendre cela un peu plus concret, nous allons implémenter les portées en JavaScript en utilisant <a href="https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Map" hreflang="en">les objets Map de ES6</a>. Nous allons donner à chaque fonction son propre tableau associatif qui sera stocké dans une variable locale nommée « portée ». (Oui, nous utilisons une variable pour implémenter des variables ; mais nous allons seulement utiliser un petit nombre fini d’entre elles, on peut <a href="http://fr.wikipedia.org/wiki/Registre_de_processeur" hreflang="fr">les voir comme des registres</a>.)</p>
<pre>
function bizarre() {
// la portée de 'bizarre' est initialement vide.
var portée = new Map;
// eval("var x = 42") correspond à l'exécution de :
portée.set('x', 42);
return function xPlus1() {
// les vars sont remontées, donc la portée initialement contient 'z'.
var portée = new Map([['z', undefined]]);
// var z = x + 1
portée.set('z', portée.get('x') + 1); // oops!
// retourne z
return portée.get('z');
}
}
</pre>
<p>Comme le commentaire l’indique, il y a un problème dans xPlus1 : x n’est pas dans la portée de la fonction xPlus1, il est dans la portée de bizarre ! Pour corriger cela nous devons faire deux choses :</p>
<ul>
<li>Ajouter une propriété englobant à toutes les portées indiquant la portée de la fonction englobante (ou l’objet global si la fonction n’est pas englobée). Dans le code précédent la fonction bizarre n’est pas englobée (car globale) mais elle englobe la fonction xPlus1.</li>
</ul>
<ul>
<li>Remplacer les utilisations de portée.get par un algorithme de recherche qui parcourt la liste chaînée de portées.</li>
</ul>
<pre>
function bizarre() {
// la portée de 'bizarre' est initialement vide.
var portée = new Map;
<font color=red>portée.englobant = window;</font>
// eval("var x = 42") correspond à l'exécution de :
scope.set('x', 42);
var tmp = function xPlus1() {
// les vars sont remontées, donc la portée initialement contient 'z'.
var portée = new Map([['z', undefined]]);
<font color=red>portée.englobant = xPlus1.englobant;</font>
// var z = x + 1
portée.set('z', <font color=red>recherche(portée, 'x')</font> + 1);
// retourne z
return recherche(portée, 'z');
}
<font color=red>tmp.englobant = portée;
return tmp;</font>
}
<font color="red">
function recherche(portée, nom) {
while (portée instanceof Map && !portée.has(nom))
portée = portée.englobant;
return portée.get(nom);
}
</font>
</pre>
<p>Notez que sans être en mesure d’utiliser des accès de variables non locales (vu que c’est ce que l’on implémente), nous devons attacher la portée de la fonction bizarre à l’objet de la fonction xPlus1. Ce n’est pas une simple astuce ; c’est une part fondamentale de l’implémentation des langages ayant <a href="http://fr.wikipedia.org/wiki/Port%C3%A9e_%28informatique%29" hreflang="fr">une portée lexicale</a> ainsi que <a href="http://en.wikipedia.org/wiki/First-class_function" hreflang="en">des fonctions du premier ordre</a>. De manière générale, nous pouvons établir la relation suivante (désolé pour l’ASCII-art) :</p>
<pre>Portée de fonction
| * ^ 0 or 1
| | englobant
| appel |
| |
V 1 | 1
Objet de fonction
| *
|
| évaluation de
|
V 1
Fonction anonyme (lambda)</pre>
<p>Chaque fonction anonyme peut être évaluée plusieurs fois, chaque évaluation produisant un objet de fonction auquel est associé sa portée de fonction. Chacun de ces objets de fonction peut être appelé de multiples fois, et chacun de ces appels produit une portée. En utilisant le langage, il est facile d’entrevoir un concept de function (lambda), mais heureusement cet exemple illustre qu’il y a en fait 3 concepts de « function » en jeu ici : la portée, l’objet, et lambda.</p>
<p>Avec ces changements, nous avons réussi à gérer les ravages de eval, mais à quel prix ? Chaque accès de variable implique un appel à un algorithme qui recherche dans des tables de hachage! Heureusement, ce problème n’est pas si différent d’un accès de propriété des objets et les mêmes solutions peuvent être utilisées : <a href="http://blog.cdleary.com/2010/09/picing-on-javascript-for-fun-and-profit/" hreflang="en">des structures cachées et des caches</a>. Je ne vais pas rentrer dans le détail de ces techniques, étant donné qu’il y a déjà plein de bonnes explications disponibles (des caches furent utilisés pour améliorer les accès de nom depuis Firefox 3). Même avec ces optimisations, la recherche des noms de variable n’est pas aussi rapide qu’on l’aurait souhaité, et nous avons toujours besoin de créer un objet Map à chaque appel.</p>
<p>En résumé, nous avons géré le pire des cas, mais nous aimerions faire mieux sur du code dans un cas plus simple.</p>
<h2>Accès rapide de variable locale</h2>
<p>Maintenant, voyons comment améliorer les accès de variables locales quand tous les accès sont locaux. Avec cette contrainte, JavaScript commence à ressembler à du C et nous pouvons utiliser les mêmes techniques qu’un compilateur C : stocker toutes les variables dans une pile et accéder à chaque variable par son emplacement dans la pile.</p>
<p>Dans une première itération (très brouillon), nous allons créer un tableau pour chaque groupe d’arguments et de variables, transformant ainsi :</p>
<pre>
foo(13, 42);
function foo(x,y) {
var a = x + y;
return bar(a);
}
</pre>
<p>en :</p>
<pre>
foo([13, 42]);
function foo(args) {
var vars = [undefined];
vars[0] = args[0] + args[1];
return bar([vars[0]]);
}
</pre>
<p>La deuxième étape consiste à éviter de créer tous ces tableaux temporaires en utilisant un grand tableau, partagé entre toutes les fonctions actives. Il y a plusieurs façons de le faire (correspondant à différentes <a href="http://en.wikipedia.org/wiki/Calling_conventions" hreflang="en">conventions d’appels</a>) ; nous allons juste voir un cas simple ici :</p>
<pre>
// exécuté avant le première appel de fonction :
var pile = [];
pile.push(13); // pousse 'x'
pile.push(42); // pousse 'y'
foo(/* nombre d'arguments poussés = */ 2);
function foo(numArgs) {
// Ajoute les arguments manquants
for (var i = numArgs; i < 2; i++)
pile.push(undefined);
// Retire les arguments en trop
for (var i = numArgs; i > 2; i--)
pile.pop();
// analogue au registre du pointeur sur cadre (en anglais <a href="http://en.wikipedia.org/wiki/Frame_pointer#Structure" lang="en">frame pointer</a>)
var cadre = pile.length;
// pousse la variable locale 'a'
pile.push(undefined);
// var a = x + y:
pile[cadre + 0] = pile[cadre - 2] + pile[cadre - 1];
// prépare la pile pour appeler 'bar(a)' :
pile.push(pile[cadre + 0]); // pousse 'a'
return bar(/* nombre d'arguments poussés = */ 1);
// dans cette convention d'appels, l'appelé dépile ses arguments.
pile.pop(); // dépile 'a'
pile.pop(); // dépile 'y'
pile.pop(); // dépile 'x'
}
</pre>
<p>Avec cette stratégie, un compilateur à la volée peut faire de bien meilleures optimisations. À commencer par le fait que chaque lecture ou écriture depuis ou vers la pile dans le code JavaScript précédent peut être compilée vers une unique lecture ou écriture du CPU. Cela est possible en cachant l’adresse de pile[cadre] dans un registre et en ajoutant <a href="http://fr.wikipedia.org/wiki/Mode_d%27adressage#Adressage_base_.2B_Offset" hreflang="fr">le « + INDEX » dans l’instruction de lecture</a>. Encore mieux, les compilateurs modernes de JavaScript peuvent utiliser un algorithme d’allocation de registre qui évite les lectures et les écritures. (Firefox intègre un allocateur de registre depuis la version 3.5.)</p>
<p>En bref, nous pouvons concevoir des accès de variables relativement efficaces, mais seulement dans un cadre bien limité.</p>
<h2>Accès rapide aux variables non locales</h2>
<p>Alors que nous ne devrions pas attendre beaucoup d’efficacité des fonctions utilisant eval et arguments, es demandes de la section précédente sont extrêmement exigeantes et rentrent en conflit à la fois avec l’<a href="http://fr.eloquentjavascript.net/chapter6.html" hreflang="fr">écriture fonctionnelle (langage)</a> et l’<a href="http://fr.eloquentjavascript.net/chapter9.html" hreflang="fr">isolation par modules</a> qui sont des classiques du développement en JavaScript. Dans cette section, on va donc s’intéresser à l’accès aux variables non locales.</p>
<p>Nous commençons par observer qu’en l’absence de eval et autres étrangetés, il n’y a nul besoin de recherche dynamique dans les portées : nous savons exactement où chercher pour accéder à la variable dans la liste des portées. La première étape consiste à considérer chaque fonction définie globalement comme un arbre de fonctions imbriquées, donnant à chaque nœud (fonction) de l’arbre un vecteur de variables définies dans sa portée.</p>
<pre>
function add3(arg1, arg2, arg3) {
function addInner(innerArg1) {
function innermost() { return innerArg1 + arg2 + getArg3() };
return innermost();
}
function getArg3() {
return arg3;
}
return addInner(arg1);
}
</pre>
<p>we can distill the following tree:</p>
<p>nous pouvons le simplifier avec l’arbre suivant:</p>
<pre>
function add3: [arg1, arg2, arg3, addInner, getArg3]
|\_ function addInner: [innerArg1, innermost]
| \_ function innermost: []
\_ function getArg3: []
</pre>
<p>L’étape suivante consiste en l’inclusion des utilisations, telles les feuilles d’un arbre, liées à la définition ayant le même nom la plus profondément imbriquée. Plutôt que de dessiner des flèche en ASCII-art, nous allons représenter un lien entre l’utilisation et la définition avec 2 coordonnées :</p>
<ul>
<li>sauts = le nombre de nœuds dans l’arbre à passer pour obtenir le nœud de la fonction qui contient la définition.</li>
<li>index = l’index de la définition dans le tableau de définition de la fonction.</li>
</ul>
<p>Lier les utilisations aux définitions dans l’arbre précédent donne :</p>
<pre>
function add3: [arg1, arg2, arg3, addInner, getArg3]
|\_ function addInner: [innerArg1, innermost]
| |\_ function innermost: []
| | |\_ "innerArg1" {hops=1, index=0}
| | |\_ "arg2" {hops=2, index=1}
| | \_ "getArg3" {hops=2, index=4}
| \_ "innermost": {hops=0, index=1}
|\_ function getArg3: []
| \_ "arg3" {hops=1, index=2}
|\_ "addInner" {hops=0, index=3}
|\_ "getArg3" {hops=0, index=4}
\_ "arg1" {hops=0, index=0}
</pre>
<pre>
function add3: [arg1, arg2, arg3, addInner, getArg3]
|\_ function addInner: [innerArg1, innermost]
| |\_ function innermost: []
| | |\_ "innerArg1" {sauts=1, index=0}
| | |\_ "arg2" {sauts=2, index=1}
| | \_ "getArg3" {sauts=2, index=4}
| \_ "innermost": {sauts=0, index=1}
|\_ function getArg3: []
| \_ "arg3" {sauts=1, index=2}
|\_ "addInner" {sauts=0, index=3}
|\_ "getArg3" {sauts=0, index=4}
\_ "arg1" {sauts=0, index=0}
</pre>
<p>Et pour la dernière étape, on supprime toutes les variables utilisées uniquement de manière locale. Nous pouvons supprimer des portées entières si elles sont vides ; nous avons juste besoin d’être attentifs à ne pas les compter parmi les sauts. Faire cette transformation produit l’arbre final suivant :</p>
<pre>
function add3: [arg2, arg3, getArg3]
|\_ function addInner: [innerArg1]
| \_ function innermost:
| |\_ "innerArg1" {hops=0, index=0}
| |\_ "arg2" {hops=1, index=0}
| \_ "getArg3" {hops=1, index=2}
|\_ function getArg3:
| \_ "arg3" {hops=0, index=1}
\_ "getArg3" {hops=0, index=2}
</pre>
<pre>
function add3: [arg2, arg3, getArg3]
|\_ function addInner: [innerArg1]
| \_ function innermost:
| |\_ "innerArg1" {sauts=0, index=0}
| |\_ "arg2" {sauts=1, index=0}
| \_ "getArg3" {sauts=1, index=2}
|\_ function getArg3:
| \_ "arg3" {sauts=0, index=1}
\_ "getArg3" {sauts=0, index=2}
</pre>
<p>Avec cette analyse, nous avons toutes les informations nécessaires pour compiler un programme efficacement. Pour les variables uniquement locales que nous avons supprimées lors de la dernière étape, nous pouvons utiliser directement la pile (comme vu dans la deuxième section). Pour les variables avec un accès non local, nous pouvons représenter une liste chaînée de portée (comme vu dans la première section), sauf que maintenant, nous représentons les portées comme des vecteurs au lieux de tableaux associatifs. Pour compiler un accès à une variable, nous utilisons les coordonnées {sauts, index} : les sauts nous donnent le nombre de portées englobantes à suivre, et l’index nous donne l’emplacement dans le vecteur de la portée.</p>
<p>Appliquer ce schéma sur l’exemple original (en omettant le mot clé arguments (manquant/en trop) qui dérange) produit le code JavaScript suivant (avec les accès aux portées mis en <font color=red>rouge</font>)</p>
<pre>
function add3() {
var cadre = arguments.length;
// la portée optimisée de add3 est: [arg2, arg3, getArg3]
var portée = [pile[cadre-2], pile[cadre-1], undefined];
portée.englobant = window;
// initialise 'addInner':
pile.push(function addInner() {
var cadre = arguments.length;
// la portée optimisée de addInner est: [innerArg1]
var portée = [pile[cadre - 1]];
portée.englobant = addInner.englobant;
// pousse la locale 'innermost'
pile.push(function innermost() {
// la portée de innermost n'existe plus après optimisation.
var portée = innermost.englobant;
// retourne innerArg1 {hops=0, index=0} +
// arg2 {hops=1, index=0} +
// getArg3() {hops=1, index=2}
return <font color=red>portée[0]</font> +
<font color=red>portée.englobant[0]</font> +
(<font color=red>portée.englobant[2]</font>)();
});
pile[cadre].englobant = scope;
// retourne innermost()
var returnValue = (pile[cadre])();
pile.pop(); // dépile 'innermost'
pile.pop(); // dépile 'innerArg1'
return returnValue;
});
pile[cadre].englobant = portée;
// initialise 'getArg3' {hops=0, index=2}:
scope[2] = function getArg3() {
// la portée de getArg3 n'existe plus après optimisation.
var portée = getArg3.englobant;
// retourne arg3 {hops=0, index=1}
return <font color=red>portée[1]</font>;
}
portée[2].englobant = portée;
// retourne addInner(arg1)
pile.push(stack[cadre - 3]);
var returnValue = (pile[cadre])();
pile.pop(); // dépile 'addInner'
pile.pop(); // dépile 'arg3'
pile.pop(); // dépile 'arg2'
pile.pop(); // dépile 'arg1'
return returnValue;
}
</pre>
<p>Les experts en performances JavaScript vont prétendre qu’ajouter une propriété nommée sur un tableau va causer une désoptimisation sur certains moteurs JavaScript (y compris SpiderMonkey, avant que le bug 586842 ne le corrige). Partons du principe que ce n’est pas le cas ; dans le cas contraire, nous pouvons simplement réserver portée<a href="https://tech.mozfr.org/post/2013/02/03/0">0</a> pour le lien vers la portée englobante.</p>
<p>Cette stratégie est bien pour la compilation à la volée de plusieurs points de vue :</p>
<ul>
<li>Si une variable est seulement accédée localement, elle peut toujours vivre sur la pile et recevoir toutes les optimisations.</li>
<li>Chaque expression .englobant est compilée en une simple instruction de chargement depuis la mémoire. De plus, quand plusieurs accès partagent la même portée, le compilateur peut les factoriser dans la même traversée de portée englobante.</li>
<li>Vu qu’un accès de variable non local est plus simple avec cette représentation qu’avec un tableau associatif, IonMonkey est capable d’optimiser les accès avec des algorithmes tels que LICM, GVN et DCE (<a href="https://tech.mozfr.org/post/2012/10/15/IonMonkey-arrive-dans-Firefox-18" hreflang="fr" title="vers article sur IonMonkey">voir article sur IonMonkey</a>)</li>
</ul>
<p>En résumé, nous avons optimisé les accès non locaux tout en conservant un accès rapide aux variables locales. Il existe d’autres optimisations liées aux portées qui permettent de réduire la chute brutale de performances pour les utilisation de eval et de arguments, mais je pense qu’il est bon de s’arrêter là.</p>
<h2>Prochaines étapes</h2>
<p>Le récent projet d’optimisation des portées nous permet de nous mettre au niveau des autres machines virtuelles JavaScript. Je devrais aussi noter que les langages fonctionnels font cela depuis des lustres. Je me réjouis à l’idée qu’il reste des optimisations simples auxquelles je pense, en partie pour <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=784839" hreflang="en">éviter de construire un objet de portée</a>, ainsi que d’autres un peu plus avancées que nous pouvons tirer des langages fonctionnels.
Ayant des idées d’optimisations simples, je me réjouis d’avance qu’elles se concrétisent bientôt ; elles permettraient entre autres d’éviter de construire un objet de portée, tandis que des améliorations plus avancées pourraient être tirées des langages fonctionnels.</p>
<h2>Dans SpiderMonkey</h2>
<p>Si cela vous intéresse de voir le code de tout ça dans SpiderMonkey, vous pouvez commencer par suivre l’un des liens suivants :</p>
<ul>
<li>Les coordonnées {sauts, index} sont appelées <a href="http://hg.mozilla.org/mozilla-central/file/df69d95f636c/js/src/vm/ScopeObject.h#l79" hreflang="en">ScopeCoordinate</a>.</li>
<li>Les divers objets de portées sont décrits par cet <a href="http://hg.mozilla.org/mozilla-central/file/2359243ee2b1/js/src/vm/ScopeObject.h#l111" hreflang="en">arbre en ASCII-art</a>. (Attention, pour des raisons purement historiques, nous utilisons la même représentation pour les objets et les portées. Suite au <a href="http://hg.mozilla.org/mozilla-central/file/df69d95f636c/js/src/jsscope.h#l32" hreflang="en">mécanisme des portées</a>, qui est généré pour les portées à la compilation, les portées sont bel et bien implémentées avec des vecteurs.)</li>
<li>L’optimisation des accès non locaux est effectuée avec l’instruction (byte-code) <a href="http://hg.mozilla.org/mozilla-central/file/df69d95f636c/js/src/jsopcode.tbl#l331" hreflang="en">ALIASEDVAR</a>. Référez-vous à ses implémentations dans l’<a href="http://hg.mozilla.org/mozilla-central/file/df69d95f636c/js/src/jsinterp.cpp#l2794" hreflang="en">interpréteur</a> et dans <a href="http://hg.mozilla.org/mozilla-central/file/df69d95f636c/js/src/ion/IonBuilder.cpp#l6272" hreflang="en">IonMonkey</a>.</li>
<li>La partie d’analyse préliminaire est un peu fouillis (et devrait être réécrite dans un futur proche). Cependant, la partie importante de cette analyse se trouve vers la fin, là où sont émises les instructions ALIASEDVAR (dans la fonction <a href="http://hg.mozilla.org/mozilla-central/file/df69d95f636c/js/src/frontend/BytecodeEmitter.cpp#l908" hreflang="en">EmitAliasedVarOp</a>).</li>
</ul>
<style>
pre { white-space: pre;}
</style>
L'assembleur du Weburn:md5:d0b9f5dd08f2e2f2111099db4dd748822013-02-19T20:30:00+01:002013-02-19T21:54:33+01:00GoofyJavaScriptInterpreteurJavaScriptWeb <p><strong><em>Re-publication du <a title="billet original de Clochix" hreflang="fr" href="http://esquisses.clochix.net/2013/02/19/asmjs/">billet de CLOCHIX sur son blog</a></em></strong></p>
<p><em>asm.js</em> est un projet de recherche de Mozilla qui vise à améliorer les performances de JavaScript en n’utilisant qu’un sous-ensemble du langage, plus facile à optimiser.</p>
<p>Il se compose de plusieurs sous-projets :</p>
<ol><li>la <a href="http://asmjs.org/">spécification du langage</a> ;</li>
<li><a href="https://wiki.mozilla.org/Javascript:SpiderMonkey:OdinMonkey">OdinMonkey</a>, un module de l’interpréteur JavaScript SpiderMonkey, qui prend en charge le code asm.js ;</li>
<li>des travaux sur <a href="https://github.com/kripken/emscripten">emscriptem</a> pour que le code généré soit conforme à la spécification asm.js ;</li>
</ol>
<h2>Asm.js</h2>
<p>La <a href="http://asmjs.org/spec/latest/">spécification</a> ne conserve qu’un sous-ensemble strict et bien typé d’EcmaScript, fonctionnant déjà dans tous les navigateurs. Elle impose par exemple que les variables incluent des indications sur leur type, afin que l’interpréteur n’ait pas à le deviner. Elle permet également une meilleure utilisation de la mémoire, limitant les ralentissements causés par le <code>garbage collector</code> (le code asm.js utilise des tableaux typés (_typed arrays_) comme pile pour certaines opérations). Elle offre aussi la possibilité d’interagir sur le DOM par l’intermédiaire de fonctions externes.</p>
<p>Pour indiquer à l’interpréteur JS que le code respecte la spécification et peut être optimisé, il suffit d’ajouter une nouvelle directive <code>"use asm"</code> de syntaxe similaire au <code>"use strict"</code>. Si l’interpréteur ne reconnait pas cette directive, il l’ignorera et interprètera le code normalement. Sinon, il validera que le code respecte bien la norme, et si oui, l’exécutera de façon optimisée.</p>
<h2>OdinMonkey</h2>
<p>OdinMonkey va <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=840282">bientôt être intégré au tronc</a> de SpiderMonkey. Les performances semblent intéressantes, puisque du code C++ compilé via Emscriptem puis interprété par OdinMonkey ne s’exécute que deux fois plus lentement que lorsqu’il est compilé avec <code>gcc -O2</code>. Ce sont des performances similaires à celles de langages utilisant une machine virtuelle, comme Java ou C#.</p>
<h2>Emscriptem</h2>
<p>Emscriptem est un compilateur qui traduit en JavaScript du <em>bytecode <a href="http://fr.wikipedia.org/wiki/LLVM">LLVM</a></em> obtenu à partir de source en C ou C++. Il permet donc de porter des programmes écrits en C ou C++ pour les exécuter dans un interpréteur JavaScript. À titre d’exemple, un portage de la bibliothèque de composants graphiques multi-plateformes Qt <a href="http://badassjs.com/post/43158184752/qt-gui-toolkit-ported-to-javascript-via-emscripten">est en cours</a>, qui permet d’exécuter des applications « natives » écrites pour Qt dans un navigateur, la balise <code>canvas</code> étant utilisée pour l’affichage. Pour tester les possibilités d’asm.js en matière de jeux, un portage du jeu de tir en vue subjective <a href="http://sauerbraten.org/">Cube 2: Sauerbraten</a> <a href="https://developer.mozilla.org/fr/demos/detail/bananabread">a été entrepris</a>.</p>
<p>D’autres expériences visent à porter les interpréteurs de divers langages. Repl.it propose d’exécuter directement dans le navigateur, après conversion en JavaScript, des programmes écrits en <a href="http://repl.it/languages">Ruby, Python, Lua, Scheme</a>…</p>
<h2>L’assembleur du Web</h2>
<p>Plus que les implications en terme de performance, l’intérêt d’asm.js est qu’il permet de faire un pas de plus vers le Web comme plate-forme applicative, dont les navigateurs seront le système d’exploitation. D’ici quelques années, il sera possible de faire fonctionner n’importe quel code, quel que soit le langage dans lequel il a été écrit, dans un navigateur.</p>
<p>Bien sûr, il faut raison garder. Il n’y a pas grand-chose de neuf sous le soleil. Le portage d’applications natives vers le navigateur est une vieille lune. Java promettait cela il y a 15 ans, avec le succès que l’on sait. GWT permet depuis des années de compiler d’autres langages en JS, et je n’ai pas l’impression qu’il soit très utilisé hors de Google. Je ne crois guère que des applications entières seront portées directement en JS. Mais je pense que cela va accroitre la perméabilité entre les environnements <em>frontend</em> et <em>backend</em>. Avec SpiderMonkey et node, du code JS écrit pour le navigateur pouvait s’exécuter sur le serveur. Bientôt, toute bibliothèque “serveur” pourra s’exécuter dans le navigateur. On pourra ainsi réellement partager du code, l’écrire dans son langage de prédilection et l’exécuter selon le contexte dans le navigateur ou sur un serveur.</p>
<h2>Quelques liens</h2>
<p>* une <a href="http://kripken.github.com/mloc_emscripten_talk/">présentation</a> d’Alon Zakai, chercheur de Mozilla et un des trois pères d’asm.js ;</p>
<p>* <a href="http://www.2ality.com/2013/02/asm-js.html">asm.js: closing the gap between JavaScript and native</a> par le toujours complet Dr. Axel Rauschmayer ;</p>
<p>* <a href="http://badassjs.com/post/43420901994/asm-js-a-low-level-highly-optimizable-subset-of">asm.js: A Low Level, Highly Optimizable Subset of JavaScript for Compilers</a>.</p>