10 points par GN⁺ 2025-08-05 | 1 commentaires | Partager sur WhatsApp
  • 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.stringify est 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.parse pour 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.stringify jusqu’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

 
GN⁺ 2025-08-05
Commentaires sur Hacker News
  • J’ai eu l’impression que l’encodage JSON constituait un gros obstacle pour la communication inter-processus dans NodeJS

    • Au final, la plupart des gens essaient de déplacer le travail vers un autre thread pour réduire la latence de l’event loop, mais se rendent compte que la charge CPU du thread principal finit quand même par être multipliée par 3
    • J’ai aussi souvent vu des exemples où l’on stringify les tableaux un par un, et il semble que ce type d’approche soit également utilisé en interne
    • J’aimerais que l’équipe V8 renforce davantage cette partie
    • Je me demande s’il est possible de traiter certains jeux de données sans bail out, et ce qu’il en est du traitement des CString ; je suis aussi curieux de voir si la fonctionnalité faststr va revenir
    • Quand j’ai commencé à analyser les performances de Node l’an dernier, JSON.stringify était l’un des plus gros facteurs limitants dans les services Node
      • Il faut utiliser stringify pour 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 streaming
      • En venant du JVM ou de Go, cela m’a paru assez amateur dans Node
    • Python a exactement le même problème
      • Je pense qu’il serait utile d’avoir des primitives IPC efficaces sous une API de haut niveau pour les cas d’usage courants
    • Je partage l’idée que l’encodage JSON est un gros obstacle pour les communications
      • Je me demande à quel point la surcharge de calcul liée au traitement du JSON dans les communications est importante à l’échelle mondiale, et s’il ne vaudrait pas mieux envoyer simplement des octets dans un format fixe ou plus efficace à parser (par ex. ASN.1)
    • Je suis contre l’idée que l’équipe V8 pousse encore plus agressivement sur ce sujet, et je recommande aux développeurs confrontés à ce problème de chercher d’autres outils
      • À mon avis, Node/V8 n’est pas particulièrement adapté au backend ni au calcul haute performance
      • JavaScript est fait pour le web et le restera longtemps, donc il n’est pas nécessaire que l’équipe V8 règle ce type de problèmes
      • L’équipe Typescript est elle aussi passée à Go, et l’automatisation de la conversion entre langages est possible
    • Il n’y a eu qu’un seul cas ou presque où déporter le travail vers un Worker m’a fait gagner plus de temps que le coût de sérialisation/désérialisation
      • Quand les données sont volumineuses, le coût élevé du passage de messages finit par annuler les bénéfices du parallélisme
  • J’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

    • Convertir une valeur flottante IEEE en chaîne UTF-8 décimale puis la reconvertir est non seulement lent, mais aussi très fragile
      • Les valeurs exactement représentables diffèrent entre le binaire et le décimal, donc de minuscules erreurs peuvent apparaître
  • Il est indiqué que le fast path ne s’applique pas si JSON.stringify reçoit un argument replacer ou space

    • Dans ce cas, je me demande si JSON.stringify(data, null, 0) peut tout de même utiliser le fast path, ou si les arguments doivent absolument être undefined
  • L’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.stringify dominait réellement le runtime dans l’écosystème V8

    • Il n’est pas nécessaire que cela représente obligatoirement la plus grande part du temps d’exécution ; comme c’est appelé sur des centaines de millions de pages chaque jour, l’effet global sur la consommation d’électricité serait considérable
  • Je pense que les performances de v8 ne sont pas assez saluées ; aujourd’hui, JS est devenu incroyablement rapide

    • C’est vraiment impressionnant ; j’y vois un bon exemple du principe « avec un milliard de dollars, on peut résoudre n’importe quel problème »
      • J’aimerais que JS évolue encore, avec quelque chose comme « strict », puis « stricter », pour devenir un langage plus simple et plus facile à compiler/JIT-er
    • À l’inverse, j’ai l’impression que v8 est optimisé à un point tel qu’il n’y a probablement qu’une centaine de personnes dans le monde qui comprennent vraiment son fonctionnement interne, et que la plupart des développeurs se demandent surtout : « pourquoi mon JS n’est-il pas rapide ? »
  • Je me demande à quel point c’est remarquable comparé aux autres écosystèmes

    • Cela fait plus de 10 ans que je fais de la sérialisation JSON, et c’est presque toujours allé si vite que je ne m’en suis jamais vraiment préoccupé
    • simdjson peut traiter des Go/s par cœur ; en tenant compte du prefetching, de la prédiction de branchement et d’autres optimisations, j’ai l’impression que la sérialisation JSON est négligeable dans la plupart des charges réelles
    • Le plus gros défaut du JSON, c’est l’overhead d’E/S ; même si le serializer est extrêmement rapide, cela ne sert à rien si l’on doit quand même écrire un blob de 100 Mo dans le stockage à chaque fois
  • « 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

    • Je me demande pourquoi
    • Est-ce que cela veut dire qu’un objet avec des clés ressemblant à des entiers serait sérialisé comme un tableau JSON ? Ce serait étonnant… non ?
  • 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

    • Je me demande si les conditions de bailout (par ex. replacer, space, .toJSON() personnalisé) sont fréquentes, et si dans ces cas on retombe immédiatement sur le chemin lent
  • V8 est remarquable, mais les performances restent inférieures à celles de LuaJIT ou de la JVM, peut-être à cause de JS lui-même

    • La JVM a certes un temps de chauffe plus long, mais malgré cela elle reste meilleure que JS
    • C’est JS qui est en cause ; je pense que V8 est bien plus avancé que luajit et la JVM
      • Java a moins de contraintes temps réel (il y a un compilateur), et c’est un avantage
    • Une grande partie de l’overhead de JS vient de sa nature dynamique
      • asm.js interdisait les comportements dynamiques comme les changements de shape des objets, ce qui permettait d’éviter de nombreuses vérifications
    • Je conteste l’expression « même la JVM » ; la JVM fait partie du très haut niveau de l’état de l’art