世界を動かす技術を、日本語で。

JSON.stringifyを2倍以上高速化した方法

概要

  • V8エンジン のJSON.stringifyが 2倍以上高速化
  • 副作用のない 新しい高速パス の導入
  • 文字列・数値変換・バッファ管理の 最適化
  • 一般的なユースケースで 自動的に恩恵
  • V8 13.8(Chrome 138) から利用可能

JSON.stringify高速化の技術的最適化

  • JSON.stringify はJavaScriptの中核的なデータシリアライズ関数
  • ネットワーク通信やlocalStorage保存など Web全体のパフォーマンス に影響
  • V8エンジンでの 大幅な高速化 により、ページ応答性・操作性向上

副作用フリーの高速パス

  • 副作用 のない場合に特化した 新しい高速パス を実装
  • 副作用=ユーザー定義コード実行やGC発生など、シリアライズ処理を妨げる要素
  • 副作用がなければ 高効率な専用処理 を適用し、従来の高コストなチェックを回避
  • 反復的(イテレーティブ)実装 により、スタックオーバーフロー対策不要・深いネストも対応

文字列表現ごとの最適化

  • V8の文字列は 1バイト(ASCII) または 2バイト(非ASCII) で管理
  • 文字種ごとに テンプレート化 し、1バイト・2バイト用に 最適化バージョン を用意
  • 文字列の種類を判定し、 必要に応じてシリアライザを切替
  • 一般的なケースで 最適化パス を維持、2バイト対応も軽量に実現

SIMDによる文字列シリアライズ高速化

  • JSONシリアライズ時、 特殊文字("や\)のエスケープ が必要
  • 長い文字列には ハードウェアSIMD命令(例:ARM64 Neon) を活用し、複数バイトを一括処理
  • 短い文字列には SWAR(SIMD Within A Register) 技法で汎用レジスタを活用
  • どちらも チャンク単位で高速スキャン、特殊文字がなければ一括コピー

エクスプレスレーン(超高速経路)の導入

  • オブジェクトのプロパティ確認処理を hidden class のフラグで最適化
  • 全プロパティが Symbolでなく、列挙可能、エスケープ不要 であれば「fast-json-iterable」マーク
  • 同じhidden classを持つオブジェクトは 追加チェックなしでキーを一括コピー
  • JSON.parse にも同様の最適化を適用、配列内の同型オブジェクトで高速化

数値→文字列変換の高速化

  • 数値の文字列化は 性能上のボトルネック
  • Grisu3アルゴリズム から Dragonbox へ置換し、最短表現の高速化
  • Number.prototype.toString() にも恩恵、全体的な数値変換の高速化

バッファ管理の最適化

  • 以前は C++ヒープ上の連続バッファ で出力を構築
  • バッファ拡張時の 再割り当て&全コピー が大きなオーバーヘッド
  • 新方式は セグメント化バッファ を採用、Zoneメモリで小分け管理
  • セグメント満杯時は新規割当てし、 高コストなコピーを排除

制限事項

  • replacer関数やspace引数 を指定すると高速パス非対応
  • .toJSON()メソッド を持つオブジェクトやプロトタイプは一般パスにフォールバック
  • インデックス付きプロパティ を持つオブジェクトも非対応
  • ConsString など一部内部文字列型は高速パス非対応
  • 通常のAPIレスポンスや設定キャッシュなど 多くのユースケースで自動的に最適化

まとめ

  • JSON.stringifyの根本的な再設計 により、JetStream2ベンチマークで 2倍以上の高速化 を達成
  • これらの最適化は V8 13.8(Chrome 138) 以降で利用可能
  • Web開発者は追加の対応不要 で自動的に恩恵を享受

Hackerたちの意見

JSONエンコーディングは、NodeJSにおけるプロセス間通信の大きな障害だよね。遅かれ早かれ、みんながNodeJSのコードでイベントループの停止を減らそうとして、別のスレッドにオフロードしようとするけど、結局メインスレッドのCPU負荷が3倍になっちゃうのを見てきた。配列を一つずつstringifyする人もいるし、今は内部でそれをやってるのかもね。もし何かあるなら、V8チームにはもっと進めてほしいな。データのサブセットでバイアウトを避けられる?CStringの問題はどうなるの?faststrが復活する可能性はある?

昨年の初めてのNodeパフォーマンス分析からの経験に基づくと、JSON.stringifyはパフォーマンスの高いNodeサービスにとって最大の障害の一つだった。みんなが辞書のキーにstringifyを使ってるし、apollo/expressがレスポンス全体を文字列にシリアライズして、少しずつストリーミングしないのも問題だと思う(これにはいくつかの回避策があるかもしれないけど、かなりハッキーに見えた)。JVM/goのバックグラウンドから来た者としては、正直言ってアマチュアっぽく感じたよ。

そうだね。オフロードすることで得られる時間が、シリアライズ/デシリアライズにかかる時間よりも多い状況は、今まで一度しか見たことないかも。重い作業をするってことは、大量のデータを扱うことが多いから、そのデータをメッセージで渡すコストは、作業を並列化するメリットに比例して増えるんだよね。

それから、処理コードをWebAssemblyにコンパイルできる何かで書けば、ArrayBuffersをワーカーにコピーして送れるようになるよ!まあ、WebAssemblyのステップなしでもできるけどね。

Pythonでも同じ問題があるよね。共通のパターンのために、効率的なIPCプリミティブとその上に高レベルのAPIがあったらいいな。

セグメントバッファのアプローチが見られて嬉しい!基本的には、ユーザーランドでfast-json-stringifyみたいなライブラリを使って手動で作ってたロープデータ構造のトリックが、今はネイティブでずっとクリーンになった感じ。バイアウト条件に頻繁に遭遇してる?リプレイサーやスペース、カスタムの.toJSON()があると、遅いパスに戻されちゃう?

一番驚いたのは、浮動小数点数のシリアライズ性能が、過去10年でどれだけ改善されたかってことだね。[1] [1] https://github.com/jk-jeon/dragonbox?tab=readme-ov-file#perf...

IEEE浮動小数点値を10進のUTF-8文字列に変換して戻すのは、遅いだけじゃなくてめっちゃ脆弱なプロセスだよ。バイナリで正確に表現できる値と、10進で正確に表現できる値の違いがあるから、小さな誤差が入り込むことがあるんだよね。

何かをシリアライズする時に一般的にどんな副作用があるのか、ちょっと困惑してる。これに関して明らかな理由のクラスがあって、俺がうっかり無視してるだけなのかな?

簡単な例としてはtoJSONがある。オブジェクトがそのメソッドを定義していれば、JSON.stringifyによって自動的に呼び出されるし、任意の副作用があるかもしれない。シリアライズ時に副作用が一般的というより、早いパスが副作用を持つ可能性のあるもの(toJSONみたいな)を避けるって感じだと思う。この記事でも簡単に触れられてるね。

プロパティゲッターを呼び出すと副作用があるから、ゲッター付きのオブジェクトをシリアライズする時は、シリアライズ中に変なことが起こらないようにめっちゃ注意しないといけないよ。確か、こういう副作用を利用してバグバウンティを取った人もいたよね。

v8ってあんまり評価されてないと思う。最近のJavaScriptがこんなに速いなんてマジでヤバいよね。

ほんと、それはすごいよね!「10億ドルあればほとんど何でも解決できる」っていういい例だと思うけど :) JavaScriptが進化し続けるのがいいな(「strict」みたいな感じで、もっと厳しく、さらに厳しく…)シンプルでコンパイルやJITが簡単な言語に進化してほしいな。

一方で、v8は変な意味で最も極端に最適化されたランタイムだと思ってる。地球上でその仕組みを理解してる人は100人くらいしかいないのに、他の人たちは「なんで俺のJS遅いの?」って感じだよね。

SWARエスケープアルゴリズム[1]は、数年前にFolly JSONで実装したやつにすごく似てる[2]。後者は4バイトじゃなくて8バイトのワードで動くし、エスケープが必要な最初のバイトの位置も返すから、エスケープが多い文字列でも速いパスに目立ったオーバーヘッドが加わらないんだよね。[1] https://source.chromium.org/chromium/_/chromium/v8/v8/+/5cbc... [2] https://github.com/facebook/folly/commit/2f0cabfb48b8a8df84f...

いつものことだけど、ダブルから文字列へのアルゴリズムの進化はだいたいJSONによって推進されてる(今回はDragonboxね)。

V8はめっちゃ良いけど、(多分JS自体のせいかな?)LuaJITやJVMのパフォーマンスにはまだ及ばないね。特にJVMは他の二つよりもウォームアップに時間がかかるし。

「even」って、ホンマに君は最高だよ。

これをいつも使ってるsafe-stable-stringifyパッケージと比べてみないといけないな。パソコンに戻ったら確認するけど、スピードアップが見られるのは嬉しいね。

JSON.parse(JSON.stringify())と再帰的な複製を使った時のオブジェクトの複製速度を比べてみたいな。