Cet article présente des détails sur la compilation en background, introduite récemment dans V8, le moteur JavaScript de Chrome.
Le dernier navigateur de Google, Chrome Beta v.33, inclut un changement important dans le moteur JavaScript V8 : la possibilité d'exécuter le processus du compilateur d'optimisation dans un thread en tâche de fond, laissant le thread principal continuer et rester réactif, pour un gain de performance important. Comme l'explique Yang Guo, Ingénieur chez Google travaillant sur le sujet, il existe deux types de compilations faites par V8 :
Afin de réduire globalement le temps passé à compiler, V8 reporte la compilation des fonctions JavaScript au moment de leur première exécution. Cette phase de compilation est rapide mais ne se focalise pas sur l'optimisation du code, elle s'attache juste à faire les choses vite. Dans V8, les portions du code qui sont exécutées fréquemment sont compilées une seconde fois par un compilateur d'optimisation spécialisé [Crankshaft]. Cette seconde passe de compilation utilise plusieurs techniques d'optimisation avancées, ce qui implique plus de temps que la première passe mais permet un code beaucoup plus rapide.
En exécutant la compilation d'optimisation dans un thread séparé, l'application est non seulement plus réactive mais aussi, d'après Guo, plus rapide de 27% sur Nexus 5 au test Mandreel de la suite de benchmark Octane 2.0.
InfoQ a réalisé des tests sur Chrome 33, d'une part avec (--js-flags="--concurrent-recompilation") et sans recompilation concurrente (--js-flags="--no-concurrent-recompilation") d'autre part et a constaté les améliorations suivantes pour les benchmarks Octane 2.0, en considérant les résultats moyens de 5 exécutions consécutives, avec un redémarrage du navigateur entre chaque :
Test | Improvement |
---|---|
Octane 2.0 (les 17 tests) | 7.12% |
Mandreel | 18% |
Box2DWeb | 32% |
zlib | 11% |
Des améliorations plus importantes ont été constatées sur les moteurs physiques 2D et 3D, bien que 7% d'amélioration aient été obtenus sur l'ensemble de la suite Octane.
Nous avons demandé à Guo pourquoi la compilation d'optimisation n'avait pas été introduite à l'occasion de la release de Crankshaft de décembre 2010. Après nous avoir bien informés qu'il ne parlait pas à la place de Google et qu'à cette époque, il n'était pas dans l'équipe, Guo explique que les améliorations sont faites en fonction des besoins avérés :
Quand Crankshaft a été conçu, la latence n'était pas vraiment un problème. Le code JavaScript doit atteindre une certaine taille pour que le temps de compilation soit vraiment notable, donc la faible latence n'était ni une question ni un objectif à la conception de Crankshaft. À mon avis, introduire la concurrence à ce moment aurait nécessité la création d'un embryon de compilateur d'optimisation inutilement compliqué et aurait été en soi une optimisation prématurée sans bénéfice immédiat.
Clairement, ceci a changé ces dernières années. Si vous regardez la nouvelle version de la suite de benchmark Octane, vous remarquerez que la taille de certaines parties dépasse le 1MB. Ceci reflète la réalité de certaines applications qui poussent les moteurs JavaScript à leurs limites. Le benchmark Mandreel consiste en un code minimisé de 4.8MB. En comparaison, le code source dézippé de Photoshop 1.0 a une taille de 4.4MB. Faire tourner cette quantité de code prend un temps significatif et devient un problème lorsque, par exemple, on s'attend à ce que le rendu d'une animation s'effectue en un clin d'oeil.
Sans chercher l'exhaustivité, Guo nous a aussi raconté les challenges à relever pour implémenter la compilation en background dans V8 :
Tout expert vous le dira, bien implémenter le multithreading est difficile. Bien couvrir par les tests est difficile. Les bugs peuvent être difficiles, voire impossibles à reproduire, à cause de la nature non-déterministe inhérente au multithreading. Disposer d'un bon jeu de cas de tests, utiliser des invariants protégés par des assertions, du "fuzz testing" sans oublier le Canary testing peuvent aider et donner confiance. Au passage, il faut remercier l'équipe ThreadSanitizer.
Avec une compilation qui bloque l'exécution, on peut être sûr que l'état du heap JavaScript, y compris de tous les objets, reste le même avant et après la compilation. Avec la compilation concurrente, cette hypothèse ne tient plus. Ceci a plusieurs implications.
V8 a un GC qui relocalise les ressources, ce qui veut dire que lorsque le GC se lance, les objets peuvent être déplacés, donc les références doivent être mises à jour. Ceci peut très bien arriver alors qu'un travail de compilation est en cours. Si les références aux objets utilisées par la tâche de compilation ne sont pas mises à jour, on tombe sur des accès mémoire invalides.
L'exécution continue pendant la compilation concurrente. Cela signifie que l'état de la VM ainsi que le layout et le contenu des objets peuvent changer de façon arbitraire. Les suppositions qui sont faites sur ces aspects au début de la compilation peuvent ne plus tenir après cette étape. Le code produit à la fin peut même ne plus être valide. L'exécuter pourrait causer des bugs et des crashs. Il faut traiter cela de façon appropriée.
En réalité, il est peu probable que le fait que le thread en background accède au tas à tout moment provoque des race conditions. On évite cela en collectant toutes les informations nécessaires au début du job de compilation.
Trouver le bon moment pour lancer un job de compilation en background est délicat : il n'y a simplement aucun moyen de prévoir de façon sûre s'il est utile d'investir du temps à optimiser une portion de code et s'il y aurait eu des bénéfices à le faire plus tôt. Formuler une solution heuristique pour s'occuper de cela est encore plus difficile. Beaucoup de réglages fins sont nécessaires et il y a encore du travail à faire.
Le cycle de vie d'une portion de code source a déjà été complexifié, il passe à travers des états interconnectés : un parsing paresseux, une première compilation avec le compilateur rapide, le code est optimisé par le compilateur d'optimisation et ensuite éventuellement dé-optimisé (dans le cas où les suppositions faites au moment de la compilation ne tiennent plus), etc. Avec la compilation concurrente, de nouveaux états viennent s'ajouter. Suivre tous ces états et assurer que le suivi des transitions ne présente pas d'anomalie et qu'il est efficace n'est pas trivial. Des cas aux limites inattendues peuvent poser problème.
D'après Guo, "V8 est sous développement actif et connait des améliorations régulières". Ceci peut être constaté en consultant la courbe de performance live maintenue par Dart, sur laquelle V8 a sauté de 30% le 11 février, sur le benchmark DeltaBlue, grâce à des optimisations du compilateur sans rapport avec la compilation en background.