10 Punkte von GN⁺ 2025-08-05 | 1 Kommentare | Auf WhatsApp teilen
  • In der V8-Engine wurde die Performance der Funktion JSON.stringify um mehr als das Doppelte gesteigert, was die Geschwindigkeit der Datenserialisierung verbessert
  • Durch die Einführung eines Optimierungspfads für Objekte ohne Seiteneffekte konnten viele defensive Prüfungen übersprungen werden, was bei typischen Datenobjekten zu großen Geschwindigkeitsgewinnen führt
  • Bei der String-Verarbeitung kamen fortgeschrittene Verfahren für Hardware und Speicher zum Einsatz, darunter die Unterscheidung zwischen 1-Byte-/2-Byte-Zeichen, die Nutzung von SIMD und Änderungen an der Struktur temporärer Puffer
  • Beim Zahlenumwandlungsprozess wurde der bisherige Grisu3-Algorithmus durch Dragonbox ersetzt, was auch bei Aufrufen von Number.toString() insgesamt schnellere Konvertierungen ermöglicht
  • Bei einigen Argumenten und Formen wird zwar zum allgemeinen Serialisierungspfad zurückgekehrt, aber in den meisten Webentwicklungs-Szenarien profitiert man automatisch von der Optimierung

Überblick

  • JSON.stringify ist eine Schlüsselfunktion zum Umwandeln von Daten in Strings in JavaScript
  • Eine Leistungssteigerung dieser Funktion wirkt sich auch positiv auf sehr wichtige Web-Aufgaben wie Netzwerkanfragen oder das Speichern in localStorage aus
  • Durch aktuelles V8-Engineering wurde die Geschwindigkeit dieser Funktion um mehr als das Doppelte verbessert, und die wichtigsten Optimierungsansätze werden im Detail vorgestellt

Fast-Path für Seiteneffektfreiheit

  • Der Kern der Optimierung ist die Anwendung eines schnellen Serialisierungspfads, der nur in Situationen ohne Seiteneffekte (side effects) verwendet werden kann
  • In solchen Fällen werden Objekte nicht rekursiv, sondern mit einer iterativen Struktur durchlaufen, wodurch keine Stack-Overflow-Prüfungen nötig sind und auch tiefere Objekte serialisiert werden können
  • Wenn ein Datenobjekt einfach ist, nutzt V8 statt der langsamen allgemeinen Logik diesen Fast Path, lässt viele Prüfungen aus und erhöht so die Geschwindigkeit

Umgang mit verschiedenen String-Darstellungen

  • V8 speichert Strings je nach 1-Byte-/2-Byte-Zeichen (ASCII/Nicht-ASCII) unterschiedlich; sobald auch nur ein Nicht-ASCII-Zeichen enthalten ist, wird der gesamte String als 2-Byte-String behandelt
  • Für die Performance der String-Serialisierung werden separate Algorithmus-Versionen je nach String-Typ kompiliert
  • Da während der Verarbeitung der Typ der String-Instanz geprüft werden muss, übernimmt bei Erkennung eines 2-Byte-Strings ein passender 2-Byte-Serializer den Zustand
  • Dadurch gibt es praktisch keinen Overhead durch Pfadwechsel je nach String-Encoding
  • Das Ergebnis wird erzeugt, indem zunächst getrennte 1-Byte- und 2-Byte-Puffer erstellt und am Ende einfach zusammengeführt werden

Optimierung der String-Serialisierung mit SIMD

  • JavaScript-Strings können Zeichen enthalten, die bei der JSON-Serialisierung maskiert werden müssen
  • Lange Strings werden mit SIMD-Hardwarebefehlen (wie ARM64 Neon) geprüft, sodass mehrere Bytes gleichzeitig verarbeitet werden
  • Kurze Strings werden mit der SWAR-Methode verarbeitet, bei der per Bit-Operationen in allgemeinen Registern mehrere Zeichen gleichzeitig behandelt werden
  • Unabhängig von der Methode kann in den meisten Fällen der gesamte String ohne besondere Umwandlung schnell kopiert werden

Express Lane hinzugefügt

  • Selbst innerhalb des Fast Path wurde eine Express Lane eingerichtet, damit die Serialisierung allein durch das Kopieren von Schlüsseln ohne wiederholte Arbeiten wie Property-Prüfungen möglich ist
  • Mithilfe des Hidden-Class-Flags eines Objekts werden Fälle, in denen Schlüssel keine Symbols enthalten, alle enumerable sind und ohne Escaping serialisiert werden können, als fast-json-iterable markiert
  • Wird später ein anderes Objekt mit derselben Hidden Class serialisiert, können die Schlüssel sofort ohne zusätzliche Prüfung kopiert werden
  • Diese Technik wird auch in JSON.parse für schnelle Schlüsselvergleiche eingesetzt

Schnellerer double-to-string-Algorithmus

  • Auch die Umwandlung von Zahlen in Strings ist häufig und komplex
  • Durch den Austausch des bisherigen Grisu3-Algorithmus gegen Dragonbox ergeben sich Leistungsverbesserungen auch bei sämtlichen Aufrufen von Number.prototype.toString()

Optimierung der Struktur temporärer Puffer

  • Beim Erzeugen von Strings wurde bisher ein einzelner zusammenhängender Puffer verwendet, was bei Platzmangel zu dem Overhead führte, den gesamten Inhalt kopieren zu müssen
  • Die neue Methode verwendet eine segmentierte Pufferstruktur, bei der je nach Bedarf mehrere kleinere Puffer aneinandergereiht werden
  • Dadurch muss bei Platzmangel nicht mehr alles kopiert werden, sondern nur ein neuer Puffer allokiert werden

Grenzen

  • Der Fast Path funktioniert nur bei einfacher Datenserialisierung
  • Werden die folgenden Bedingungen nicht erfüllt, wird der allgemeine Pfad verwendet
    • replacer- oder space-Argumente dürfen nicht verwendet werden (kein Pretty-Print, keine Transformation)
    • Es muss sich um ein einfaches Objekt ohne benutzerdefinierte toJSON-Methode handeln
    • Bei indexbasierten Properties wird auf den langsamen Pfad gewechselt
    • ConsString und andere spezielle Strings werden nicht verarbeitet
  • Für die meisten üblichen Einsatzfälle wie Datenserialisierung, API-Antworten oder Konfigurations-Caching greift die Optimierung automatisch

Fazit

  • Durch eine Neugestaltung des Ansatzes in allen Bereichen – von der Grundarchitektur von JSON.stringify über Speicherverarbeitung bis zur Zeichenverarbeitung – wurde im JetStream2-Benchmark eine mehr als doppelte Geschwindigkeitssteigerung erzielt
  • Diese Verbesserungen sind ab V8 Version 13.8 (Chrome 138) direkt nutzbar

1 Kommentare

 
GN⁺ 2025-08-05
Hacker-News-Kommentare
  • Ich habe festgestellt, dass JSON-Encoding in NodeJS ein großes Hindernis für die Kommunikation zwischen Prozessen ist

    • Meist versucht man am Ende, Arbeit an andere Threads auszulagern, um die Verzögerung der Event Loop zu verringern, stellt dann aber fest, dass die CPU-Last des Main-Threads am Ende auf das Dreifache steigt
    • Ich habe auch viele Beispiele gesehen, bei denen Arrays einzeln stringify-ed werden; es wirkt, als würde intern ein ähnlicher Ansatz verwendet
    • Ich hoffe, das V8-Team verstärkt diesen Bereich noch weiter
    • Mich würde interessieren, ob sich manche Datensätze ohne Bailout verarbeiten lassen und was mit der CString-Behandlung ist; auch ob das faststr-Feature zurückkehrt
    • Als ich letztes Jahr zum ersten Mal Node-Performance analysiert habe, war JSON.stringify einer der größten Faktoren, die die Performance eines Node-Dienstes ausgebremst haben
      • Man muss stringify für Dictionary-Keys verwenden, und apollo/express serialisieren die gesamte Antwort als einen einzigen String statt sie zu streamen
      • Wenn man von der JVM oder Go kommt, wirkte so etwas in Node ziemlich amateurhaft
    • Python hat genau dasselbe Problem
      • Ich denke, es wäre gut, effiziente IPC-Primitiven unter einer High-Level-API für gängige Muster zu haben
    • Ich stimme der Meinung zu, dass JSON-Encoding ein großes Hindernis für Kommunikation ist
      • Ich frage mich, wie groß der Rechen-Overhead durch JSON-Verarbeitung in der weltweiten Kommunikation insgesamt ist und ob es besser wäre, Bytes einfach in einem festen, parserfreundlicheren Format zu senden, etwa ASN.1
    • Ich bin dagegen, dass das V8-Team diesen Bereich noch aggressiver optimiert, und würde Entwicklern mit diesem Problem empfehlen, andere Werkzeuge zu suchen
      • Ich halte Node/V8 für Backend- oder Hochleistungsrechenprobleme nicht für besonders geeignet
      • JavaScript ist auf Web-Anwendungen ausgerichtet und wird das noch lange bleiben, daher muss das V8-Team solche Probleme nicht lösen
      • Auch das TypeScript-Team ist zu Go gewechselt, und automatische sprachübergreifende Konvertierung ist ebenfalls möglich
    • Es gab fast nur ein einziges Mal, dass das Auslagern auf Worker mehr Zeit gespart hat als durch Serialisierung/Deserialisierung verloren ging
      • Wenn die Daten groß sind, ist der teure Nachrichtentransfer ähnlich groß wie der Gewinn durch Parallelisierung
  • Es war wirklich erstaunlich, wie stark sich die Performance der Serialisierung von Gleitkommazahlen in den letzten etwa zehn Jahren verbessert hat

    • Das Umwandeln von IEEE-Gleitkommawerten in dezimale UTF-8-Strings und wieder zurück ist nicht nur langsam, sondern auch sehr fragil
      • Da sich die exakt darstellbaren Werte in Binär- und Dezimalsystem unterscheiden, können kleine Fehler entstehen
  • Es heißt, dass in JSON.stringify bei gesetztem replacer- oder space-Argument der Fast Path nicht angewendet wird

    • Dann würde mich interessieren, ob JSON.stringify(data, null, 0) den Fast Path trotzdem nutzen kann oder ob die Argumente wirklich undefined sein müssen
  • Der SWAR-Escaping-Algorithmus[1] ist dem sehr ähnlich, was ich in Folly JSON implementiert hatte[2]

  • Ich bezweifle nicht den Wert der Arbeit selbst, aber mich würden konkrete Probleme oder Daten interessieren, bei denen JSON.stringify in der realen V8-Umgebung tatsächlich die Laufzeit dominiert hat

    • Der Anteil an der Ausführungszeit muss nicht zwingend überwältigend sein; wenn das täglich auf hunderten Millionen Seiten aufgerufen wird, wäre der weltweite Stromspareffekt dennoch beträchtlich
  • Ich denke, die Performance von v8 wird nicht genug gewürdigt; modernes JS ist inzwischen unglaublich schnell geworden

    • Wirklich beeindruckend, ein gutes Beispiel für „Mit einer Milliarde Dollar kann man jedes Problem lösen“
      • Ich hoffe, dass sich JS künftig weiterentwickelt, etwa in Richtung „strict“ und „stricter“, damit es zu einer einfacheren Sprache für Compiler/JIT wird
    • Andererseits ist v8 so extrem optimiert, dass es weltweit vielleicht nur rund 100 Menschen gibt, die das Innere wirklich verstehen, und die meisten Entwickler wohl eher denken: „Warum ist mein JS nicht schnell?“
  • Mich würde interessieren, wie herausragend das im Vergleich zu anderen Ökosystemen wirklich ist

    • Ich serialisiere seit über zehn Jahren JSON, aber es war fast immer so schnell, dass ich mir darüber nie Gedanken gemacht habe
    • simdjson schafft GB pro Sekunde und Core, und wenn man Optimierungen wie Prefetching und Branch Prediction berücksichtigt, ist JSON-Serialisierung in den meisten realen Workloads vermutlich vernachlässigbar
    • Der größte Nachteil von JSON ist der IO-Overhead; egal wie schnell der Serializer ist, wenn man jedes Mal einen 100-MB-Blob in den Speicher schreiben muss, bringt das wenig
  • „No indexed properties on objects“ — der Fast Path scheint also nur für normale Objekte mit stringbasierten Keys optimiert zu sein; wenn array-ähnliche Index-Properties vorhanden sind, geht es auf den Slow Path zurück

    • Ich frage mich, warum das so ist
    • Bedeutet das, dass Objekte mit integer-ähnlichen Keys als JSON-Arrays serialisiert werden? Das kann doch nicht wirklich so sein …?
  • Mir gefällt der Ansatz mit segmentierten Buffern; früher musste man Rope-Tricks mit Userland-Bibliotheken wie fast-json-stringify selbst bauen, jetzt ist das nativ natürlich viel besser

    • Mich würde interessieren, wie oft Bailout-Bedingungen auftreten, etwa replacer, space oder ein benutzerdefiniertes .toJSON(); springt es in solchen Fällen sofort auf den langsamen Pfad zurück?
  • V8 ist hervorragend, aber entweder wegen JS selbst oder aus anderen Gründen liegt die Performance hinter LuaJIT oder der JVM zurück

    • Die JVM hat zwar eine lange Aufwärmzeit, ist aber trotzdem besser als JS
    • JS ist die Ursache; ich denke, V8 ist deutlich fortschrittlicher als LuaJIT und die JVM
      • Java hat weniger Echtzeitbeschränkungen (es gibt einen Compiler), und das ist ein Vorteil
    • Viel JS-Overhead kommt von seiner Dynamik
      • asm.js konnte viele Checks überspringen, weil dynamisches Verhalten wie Änderungen an der Shape von Objekten verboten war
    • Ich würde der Formulierung „sogar die JVM“ widersprechen, die JVM gehört zur Spitzenklasse