Comment WebAssembly peut exécuter JavaScript rapidement
(bytecodealliance.org)Intro
-
Quand on exécute du JS dans le navigateur, le moteur JS du navigateur est très bien optimisé, donc l’exécution est rapide, mais aujourd’hui on utilise aussi beaucoup JS dans d’autres environnements. (serverless, consoles de jeu, iOS, etc.)
-
WASM est une technologie qui permet de faire tourner rapidement JS dans ces runtimes.
Fonctionnement
-
S’il existe un moteur JS, le code JS est transformé en bytecode via un interpréteur, un compilateur JIT, etc.
-
Dans un environnement sans moteur JS, il faut distribuer le moteur JS avec le code ; en distribuant le moteur JS sous forme de module WASM, on peut le rendre portable sur divers environnements.
-
Le code JS s’exécute à l’intérieur d’un moteur JS isolé au sein du moteur WASM.
-
Le moteur JS utilisé par le moteur WASM est SpiderMonkey, celui qu’utilise aussi Firefox.
-
WASM ne peut pas, à lui seul, produire du code machine, il doit donc passer par une compilation vers JS.
-
Mais comme il ne peut pas utiliser le JIT, il est normal que WASM soit plus lent. Alors, comment WASM peut-il au juste « accélérer » l’exécution de JS ?
Où utilise-t-on WASM ?
Utiliser JS sur iOS (ou dans des environnements sans JIT)
- Les consoles de jeu, les apps iOS non privilégiées, les smart TV, etc. ne peuvent pas utiliser le JIT pour des raisons de sécurité.
(→ Le texte en parle comme si les problèmes de sécurité du JIT allaient de soi, mais même en cherchant je n’ai pas très bien trouvé pourquoi.)
- Dans ces cas, il faut donc utiliser un interpréteur, mais en réalité les applications qui tournent sur ces plateformes restent souvent actives très longtemps et contiennent beaucoup de code, donc il vaut mieux éviter le ralentissement lié à l’interpréteur.
- Comment utiliser JS tout en évitant la baisse de performances due à l’interpréteur ?
Utiliser JS en serverless
- En environnement serverless, le JIT existe, mais le problème est le temps de cold start, qui allonge la latence. (au moins 5 ms rien que pour charger le moteur)
- Il existe des techniques d’optimisation pour masquer le cold start, mais à mesure que la couche réseau s’améliore (p. ex. QUIC), elles perdent en intérêt, et elles deviennent aussi peu utiles lorsqu’on exécute plusieurs fonctions serverless en même temps.
- On peut aussi éviter le cold start en réutilisant les instances, mais cela signifie que l’état est partagé entre les requêtes, ce qui crée un risque de sécurité.
- À cause de cela, en pratique, on voit aussi de plus en plus de cas où l’on entasse beaucoup de logique dans une seule fonction serverless, sans suivre les best practices.
- Autrement dit, si l’on résout uniquement le problème du cold start, on n’a plus besoin de diverses techniques destinées à l’éviter, et beaucoup d’autres problèmes disparaissent aussi.
- WASM encapsule et isole JS ; comme le code propre à WASM est court et simple, il est aussi plus facile à surveiller, et les risques de sécurité sont réduits.
Où un moteur JS passe-t-il le plus de temps ?
Phase d’initialisation
- (initialisation du moteur) Cela correspond au serverless. Le moteur doit se préparer lui-même et ajouter les fonctions built-in à l’environnement. C’est l’une des raisons pour lesquelles le cold start du serverless est lent.
- (initialisation de l’application) parsing des fonctions en bytecode, allocation mémoire pour les variables, affectation des valeurs aux variables
Phase d’exécution
- À partir de là, le throughput dépend de plusieurs conditions.
- which language features are used
- whether the code behaves predictably from the JS engine’s point of view
- what sort of data structures are used
- whether the code runs long enough to benefit from the JS engine’s optimizing compiler
Accélérer un moteur JS signifie accélérer ces deux phases, l’initialisation et l’exécution. Plus précisément, cela consiste à réduire le temps d’initialisation et à augmenter le throughput à l’exécution, c’est-à-dire la vitesse de traitement du code.
Réduire le temps d’initialisation
-
WASM réduit le temps d’initialisation en utilisant un pre-initializer appelé Wizer. (sur une petite application, JS on WASM est environ 13 fois plus rapide qu’un isolate JS)
-
Lors de l’étape de build, avant de déployer le code, le pre-initializer exécute une fois tout le code JS jusqu’à la phase d’initialisation.
-
Ainsi, le code JS est déjà stocké sous forme de bytecode dans la mémoire linéaire du moteur JS, et l’allocation mémoire est déjà terminée.
-
Il suffit alors de copier cela tel quel et de l’ajouter à la section de données de WASM.
-
-
Lorsque le moteur JS est instancié, il peut accéder à toutes les données de la section de données. S’il a besoin d’une zone mémoire spécifique, il peut la copier depuis cette section. Il n’y a donc pas besoin de temps de démarrage, d’où le terme pre-initialization.
-
Pour l’instant, la section de données est attachée au même module que le moteur JS, mais à l’avenir il est prévu d’utiliser le module linking pour en faire un module séparé, afin que plusieurs applications puissent partager le même moteur JS.
-
En réalité, cette technique de pré-initialisation n’a pas besoin de se limiter aux moteurs JS ; c’est un concept qui peut s’appliquer à n’importe quel runtime, comme Python, Ruby ou Lua.
Augmenter le throughput
-
Si le code JS ne s’exécute que très peu de temps, il ne passe de toute façon pas par le JIT, donc le throughput de WASM sera similaire à celui du navigateur. En revanche, pour du code qui s’exécute longtemps, l’usage ou non du JIT entraîne un écart important de throughput.
-
Comme WASM ne peut pas utiliser le JIT, il adopte à la place une compilation AOT (ahead-of-time), tout en reprenant quand même certaines techniques du JIT.
-
L’une des techniques d’optimisation du JIT est l’inlining cache : conserver des fragments de code déjà exécutés pour les réutiliser ensuite.
-
Dans WASM, les patterns fréquemment utilisés en JS sont préparés sous forme de stubs. Par exemple, l’accès aux propriétés d’un objet.
-
Normalement, pour gérer correctement l’accès à une propriété d’objet, il faut connaître les informations de shape et d’offset, ce qu’on ne peut pas savoir en AOT.
-
En revanche, on peut préparer à l’avance un stub qui accède à la propriété en prenant shape et offset comme paramètres. Ce code stub peut être réutilisé à plusieurs endroits.
-
-
WASM prépare ainsi tous ces common patterns sous forme de stubs. Cela ne dépend pas de la forme réelle du code JS. Cela permet de réduire la quantité de code machine que le moteur JS doit produire, de raccourcir le temps d’initialisation et d’améliorer la localité de cache.
-
Il a été constaté qu’avec seulement 2 kb de ces stubs, on peut couvrir environ 95 % du code JS réel.
-
Comme cette technique optimise ahead-of-time, c’est-à-dire sans connaître le contenu du code (et sans profiling), il resterait probablement de la marge pour aller plus loin, comme avec le JIT, si l’on faisait davantage de profiling.
- Mais le profiling lui-même n’est pas simple, donc des travaux sont en cours.
2 commentaires
Concernant les problèmes de sécurité liés au JIT, un billet de blog de l’équipe MS Edge, déjà présenté ici auparavant, mentionnait ce sujet. En règle générale, les moteurs JIT sont complexes, ce qui augmente la surface d’attaque. De plus, les méthodes appliquées par le JIT pour améliorer les performances, comme l’optimisation spéculative (Speculative Optimization), semblent avoir tendance à provoquer de manière répétée certains types de problèmes de sécurité. C’est pour cette raison que la proportion de failles de sécurité liées au JIT parmi les failles de sécurité des navigateurs web serait assez élevée.
https://fr.news.hada.io/topic?id=4771
https://microsoftedge.github.io/edgevr/posts/Super-Duper-Secure-Mode/
https://docs.google.com/spreadsheets/d/…
Oh merci ! Je n'avais même pas pensé à aller voir GeekNews.