exppad* / blog / Esthétique d'asm.js June 15, 2014

J'avais jusque là souvent croisé le nom d'asm.js mais je n'étais jamais allé voir précisément à quoi ça ressemble jusqu'à ce matin.

Petit rappel

L'idée d'asm.js est d'être un sous-ensemble de javascript permettant de faire tout un tas d'optimisations pour rendre l'interprétation plus rapide, notamment en compilant un max de code en langage machine à l'avance (AOT compilation). Ça donne lieu à des démos un peu folles comme le portage d'Unreal Engine sur Firefox ou encore celui de Qt et on entend dire que c'est seulement deux fois plus lent que le code C d'origine.

Mais alors attention, asm.js n'est pas fait pour être utilisé à la main ! Comme son nom le suggère (asm = assembleur), il est prévu pour être généré à partir d'un code dans un autre langage. Le compilo le plus utilisé pour ça, c'est emscripten. Pour éviter d'avoir à coder un compilateur par langage, emscripten compile depuis le langage très bas niveau de LLVM qui est déjà la cible de compilateurs de pas mal de langages (C, C++ évidemment, mais aussi Haskell, Fortran, etc). Et hop, on peut « faire tourner du C » dans le navigateur. C'est beau.

Regardons de plus près

C'est pas parce qu'on me dit de pas le faire que j'ai pas eu envie de coder en asm.js à la main. Juste pour voir… Donc je vais fièrement lire la spec d'asm.js. Et ça se résume assez vite : c'est du hack !

On pouvait s'y attendre, me direz-vous. Mais à ce point là… Le fait est que JavaScript a un typage douteux (Oh, wait! Il en a pas du tout en fait) et du coup pour compiler tout ça de façon efficace — enfin pour compiler tout cours — c'est assez chaud. Il a donc fallu trouver un moyen subtile de signaler le type des objets manipulés.

Prenons un exemple simple : une fonction qui somme deux entiers :

function sum(x, y) {
    return x + y;
}

Hmm et si on l'appelle avec autre chose que des entiers, genre des chaînes de caractères ? Eh bien si on compile ça naïvement, on va sommer des pointeurs et ça va faire une belle segfault. Sinon, on vérifie au runtime le type, mais alors bonjour les performances…

Voici donc l'astuce :

function sum(x, y) {
    x = +x; // Le + unaire retourne focément un nombre.
    y = +y;
    return +(x + y); // oui oui, même là faut préciser
}

Subtilement, je vous disais ! En fait, il faut systématiquement indiquer forcer le type espéré à l'appel de fonction, donnant lieu à des trucs immondes comme +sqrt(+square(x) + +square(y)). Je sais pas vous, mais moi ça me rappelle JSF*ck et ses variantes.

On avance un peu, et on a envie d'accéder à un élément de tableau, un simple array[i]. Et si i n'était pas un entier ? Ah, je sais : array[+i] ! Eh bien… non, array[+3.14] ne veut rien dire par exemple. La solution est donc array[i|0]. On fait un ou bit à bit avec 0 (l'élément neutre) pour être sûr d'avoir un entier.

Et ne vous avisez pas de faire du zèle en remplaçant le |0 par un <<0. C'aurait le même effet en JavaScript normal, mais ça perd complètement le typeur d'asm.js !

Ajoutons à ça le fait que pour être compilée en tant qu'asm.js, une fonction ne doit pas prendre plus de trois arguments et que le premier doit toujours être window lors de l'appel de la fonction et vous obtenez vraiment un langage caché dans un autre.

Asm.js est évidemment contraint de respecter la syntaxe de JavaScript pour que tout se passe bien pour les navgateurs ne le supportant pas, mais on obtient du coup un code vraiment obscure.

Ce qui m'embête

Ce qui m'embête, c'est que les devs s'appliquent à coder un moteur rapide pour asm.js. Ils y passent un temps fou et on peut imaginer que du coup ils passent moins de temps à perfectionner le « vrai » JavaScript.

Et puis si on y réfléchit deux secondes, c'est quand même fou ce qui se passe :

  • On a un code C.
  • On compile ce code vers un langage très bas niveau, celui de LLVM.
  • On compile ce quasi assembleur en un autre (asm.js), mais en ajoutant une contrainte : être correct en… JavaScript.
  • On parse ce code javascript pour tenter à nouveau de le convertir en langage machine.
  • On exécute enfin le tout, ouf !
N'y aurait-il pas un raccourci ?

N'y aurait-il pas un raccourci ?

Qui ne trouve pas qu'il y a trop d'étapes là ? Enfin c'est pas tant leur nombre que leur inintérêt qui me choque !

Alors attention hein, je ne dis pas de ne pas utiliser asm.js, au contraire, c'est beau que ça marche. Mais ça montre encore une fois que de façon générale, plus personne ne veut faire de JavaScript et tout le monde s'évertue à développer des langages compilés vers JavaScript, comme CoffeScript, TypeScript, etc ou même des nouveaux langages comme Google Dart.

On va avoir du mal à abandonner JavaScript, pour des raisons évidentes de rétro-compatibilité en particulier, mais petit à petit plus personne ne va l'utiliser directement à ce rythme là. Les moteurs seront optimisés sur un tout petit sous-ensemble du langage pour avoir un truc efficace et on se trimbalera toute la syntaxe et le reste du langage juste pour les anciens moteurs.

C'est une étape nécessaire, mais j'espère qu'elle mènera bien vers le remplacement de JavaScript qu'elle est le seul à pouvoir faire progressivement.

On pourrait par exemple imaginer communiquer directement le langage de LLVM plutôt que de recoder une VM entière dans les navigateurs. Il a l'avantage d'être compatible avec énormément d'architectures puisque c'est pour ça que LLVM a été fait — être un intermédiaire de compilation pour éviter de faire un compilo par archi et par langage.

Le seul truc qu'on va perdre, et c'est déjà le cas avec asm.js, c'est l'accès aux sources. Un des avantages des technos web était que l'on peut toujours jeter un coup d'œuil sous le capot, pour s'en insiprer ou faire quelques modifs à la volée.

J'attends donc de voir ce qui va se passer, mais je suis toujours aussi fasciné de voir l'ampleur du travail causé par les problèmes de rétro-compatibilité du web !