- 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.stringifyist 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-iterablemarkiert - 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.parsefü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
Hacker-News-Kommentare
Ich habe festgestellt, dass JSON-Encoding in NodeJS ein großes Hindernis für die Kommunikation zwischen Prozessen ist
stringify-ed werden; es wirkt, als würde intern ein ähnlicher Ansatz verwendetfaststr-Feature zurückkehrtJSON.stringifyeiner der größten Faktoren, die die Performance eines Node-Dienstes ausgebremst habenstringifyfür Dictionary-Keys verwenden, und apollo/express serialisieren die gesamte Antwort als einen einzigen String statt sie zu streamenEs war wirklich erstaunlich, wie stark sich die Performance der Serialisierung von Gleitkommazahlen in den letzten etwa zehn Jahren verbessert hat
Es heißt, dass in
JSON.stringifybei gesetztemreplacer- oderspace-Argument der Fast Path nicht angewendet wirdJSON.stringify(data, null, 0)den Fast Path trotzdem nutzen kann oder ob die Argumente wirklichundefinedsein müssenDer 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.stringifyin der realen V8-Umgebung tatsächlich die Laufzeit dominiert hatIch denke, die Performance von v8 wird nicht genug gewürdigt; modernes JS ist inzwischen unglaublich schnell geworden
Mich würde interessieren, wie herausragend das im Vergleich zu anderen Ökosystemen wirklich ist
„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
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
replacer,spaceoder 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