- Dans le moteur V8, les performances de la fonction JSON.stringify ont été plus que doublées, améliorant ainsi la vitesse de sérialisation des données
- Une voie d’optimisation a été introduite pour les objets sans effets de bord, ce qui permet d’omettre de nombreuses vérifications défensives et d’obtenir de forts gains de vitesse sur les objets de données courants
- Pour le traitement des chaînes, des méthodes avancées ont été appliquées à l’échelle du matériel et de la mémoire, avec distinction entre 1 octet et 2 octets, usage de SIMD et modification de la structure des buffers temporaires
- Dans le processus de conversion des nombres, l’ancien algorithme Grisu3 a été remplacé par Dragonbox, ce qui ouvre la voie à des conversions plus rapides dans l’ensemble des appels à
Number.toString() - Pour certains arguments et certaines formes de données, le chemin de sérialisation standard reste utilisé, mais dans la plupart des situations de développement web, l’effet de l’optimisation s’applique automatiquement
Vue d’ensemble
JSON.stringifyest une fonction clé qui convertit des données JavaScript en chaîne de caractères- L’amélioration de ses performances a aussi un effet positif sur des opérations très importantes sur le web, comme les requêtes réseau ou l’enregistrement dans localStorage
- Grâce aux dernières avancées d’ingénierie de V8, la vitesse de cette fonctionnalité a été plus que doublée, et les principales optimisations sont présentées en détail
Fast Path sans effets de bord
- Le cœur de l’optimisation consiste à appliquer un chemin de sérialisation rapide utilisable uniquement dans les cas sans effets de bord
- Dans ces situations, les objets sont parcourus avec une structure itérative plutôt que récursive, ce qui évite d’avoir à vérifier les dépassements de pile et permet aussi de tenter la sérialisation d’objets plus profonds
- Quand un objet de données est simple, V8 exploite ce Fast Path au lieu de la logique générale plus lente, en supprimant de nombreux contrôles pour accélérer le traitement
Gestion des différentes représentations de chaînes
- V8 stocke différemment les chaînes selon qu’elles utilisent des caractères 1 octet / 2 octets (ASCII / non-ASCII), et s’il existe ne serait-ce qu’un caractère non ASCII, l’ensemble est géré en 2 octets
- Pour les performances de sérialisation, des versions distinctes de l’algorithme sont compilées selon le type de chaîne
- Comme il faut vérifier le type concret de l’instance de chaîne pendant le traitement, lorsqu’une chaîne 2 octets est détectée, un sérialiseur 2 octets adapté prend le relais
- Grâce à cela, le coût du basculement de chemin selon l’encodage des chaînes est pratiquement nul
- Le résultat est produit en créant séparément des buffers 1 octet et 2 octets, puis en les fusionnant simplement à la fin
Optimisation de la sérialisation des chaînes avec SIMD
- Les chaînes JavaScript peuvent contenir des caractères qui doivent être échappés lors de la sérialisation JSON
- Les chaînes longues sont analysées plusieurs octets à la fois grâce à des instructions matérielles SIMD (comme ARM64 Neon)
- Les chaînes courtes sont traitées selon une approche SWAR, qui utilise des opérations sur les bits dans des registres généraux pour traiter plusieurs caractères simultanément
- Quelle que soit la méthode, dans la plupart des cas il est possible de copier rapidement toute la chaîne sans transformation particulière
Ajout d’Express Lane
- Même au sein du Fast Path, une voie ultra-rapide a été ajoutée pour permettre une sérialisation par simple copie des clés, sans travail répétitif comme les vérifications de propriétés
- En exploitant le flag de hidden class des objets, lorsqu’une clé ne contient pas de Symbol, que toutes sont enumerable et déjà sérialisables sans échappement, l’objet est marqué comme
fast-json-iterable - Lors de la sérialisation d’un autre objet ayant la même hidden class, la copie des clés est effectuée directement sans vérification supplémentaire
- Cette technique est aussi réutilisée dans
JSON.parsepour accélérer la comparaison des clés
Un algorithme double-to-string plus rapide
- Le processus de conversion des nombres en chaînes est lui aussi fréquent et complexe
- L’ancien algorithme Grisu3 a été remplacé par Dragonbox, ce qui améliore également les performances sur l’ensemble des appels à
Number.prototype.toString()
Optimisation de la structure des buffers temporaires
- Lors de la construction des chaînes, on utilisait auparavant un buffer continu unique, ce qui entraînait une surcharge liée à la copie complète à chaque manque d’espace
- La nouvelle méthode repose sur une structure de buffers segmentés, qui assemble plusieurs petits buffers selon les besoins
- Ainsi, en cas de manque d’espace, il suffit d’allouer un nouveau buffer sans recopier l’ensemble
Limites
- Le Fast Path ne fonctionne que pour les sérialisations de données simples
- Si les conditions ci-dessous ne sont pas remplies, le chemin standard est utilisé
- impossible d’utiliser les arguments replacer ou space (pas de Pretty-Print ni de transformation)
- il faut un objet simple, sans méthode personnalisée toJSON
- en présence de propriétés indexées, le traitement bascule vers le chemin lent
- les chaînes spéciales comme ConsString ne sont pas prises en charge
- Pour la plupart des usages courants, comme la sérialisation de données, la génération de réponses d’API ou la mise en cache de configuration, l’optimisation s’applique automatiquement
Conclusion
- En repensant l’approche sur toute la chaîne, de la conception de base de
JSON.stringifyjusqu’au traitement mémoire et caractère, V8 atteint un gain de plus de 2× sur le benchmark JetStream2 - Ces améliorations sont disponibles à partir de V8 13.8 (Chrome 138)
1 commentaires
Commentaires sur Hacker News
J’ai eu l’impression que l’encodage JSON constituait un gros obstacle pour la communication inter-processus dans NodeJS
stringifyles tableaux un par un, et il semble que ce type d’approche soit également utilisé en interneJSON.stringifyétait l’un des plus gros facteurs limitants dans les services Nodestringifypour les clés de dictionnaire, et apollo/express sérialise toute la réponse en une seule chaîne au lieu de la diffuser en streamingJ’ai été très surpris de voir à quel point les performances de sérialisation des nombres à virgule flottante se sont améliorées ces 10 dernières années
Il est indiqué que le fast path ne s’applique pas si
JSON.stringifyreçoit un argumentreplacerouspaceJSON.stringify(data, null, 0)peut tout de même utiliser le fast path, ou si les arguments doivent absolument êtreundefinedL’algorithme d’escaping SWAR[1] ressemble beaucoup à celui que j’avais implémenté dans Folly JSON[2]
Je ne remets pas en cause la valeur du travail lui-même, mais j’aimerais voir des problèmes ou des données plus concrets montrant que
JSON.stringifydominait réellement le runtime dans l’écosystème V8Je pense que les performances de v8 ne sont pas assez saluées ; aujourd’hui, JS est devenu incroyablement rapide
Je me demande à quel point c’est remarquable comparé aux autres écosystèmes
« No indexed properties on objects » — il est dit que le fast path n’est optimisé que pour les objets ordinaires avec des clés sous forme de chaînes ; s’il y a des propriétés indexées de type array-like, on repasse sur le slow path
J’aime bien l’approche par segmented buffer ; auparavant, il fallait bricoler soi-même des astuces de rope avec des bibliothèques userland comme fast-json-stringify, alors qu’ici c’est natif, ce qui est bien mieux
replacer,space,.toJSON()personnalisé) sont fréquentes, et si dans ces cas on retombe immédiatement sur le chemin lentV8 est remarquable, mais les performances restent inférieures à celles de LuaJIT ou de la JVM, peut-être à cause de JS lui-même