ブログに戻る
GCJSONperformancebenchmarksmilestone

世代別GC、遅延JSON、そして精査に耐えるベンチマーク

前回の記事はv0.5.174で締めくくられ、見出しは1つでした:Perryはついに、ツリー内スイートのすべてのベンチマークでNodeとBunの両方に勝っていました。3日間の作業とGC + JSONコミットのバックログを経て、Perryはv0.5.306に到達しました――これは132回のパッチリリースです――そしてストーリーは別のものになっています。見出しは547倍のスピードアップでもなければ、新しい勝ち列でもありません。それらの勝利を擁護可能なものにする作業です。

  • 世代別GCがデフォルトとして出荷されます。Phase A〜Dはv0.5.217〜v0.5.237にわたって着地しました。
  • Small String Optimizationがデフォルトとして出荷されます。Step 1.5 → 2はv0.5.213〜v0.5.216で着地しました。
  • JSONパイプラインはテープベースのパーサ、遅延パース、遅延ストリンギファイ、要素ごとの疎なマテリアライズを得ました。デフォルトのvalidate-and-roundtripは今や中央値75 ms――動的型付けの中で最速です。
  • ベンチマークページは端から端まで書き直され、RUNS=11 中央値 + p95 + σ + min + max、ピアとして追加されたsimdjsonとAssemblyScript+json-as、本物の比較から分離された最適化プローブ、そしてPerryが持つすべての弱点を正直に表面化しました。

脇役は正しさの修正の安定した一連の流れです:Promiseマイクロタスク FIFO、NaN等価性とECMAScript数値フォーマット、BigInt 2の補数、AsyncLocalStorageのエンドツーエンド、decimal.js + ioredis + commanderランタイム、そしてテープパスの下に隠れていた素のf64でのJSON.stringifyのsegfault。加えて、Windowsツールチェーンがついに軽量化されました:LLVM + xwin、Visual Studioのインストールは不要です。

1. デフォルトオンの世代別GC

世代別GCは2か月間にわたる段階的なロールアウトでした。この期間に閉じたフェーズの要約:

  • v0.5.217〜v0.5.221 ― Phase A:シャドウスタックランタイムの足場、push/pop発行、スロットマップスレッディング、Let/LocalSetシャドウミラーリング、ルートスキャナ。
  • v0.5.222 ― Phase B:ナーサリ + 旧世代アリーナ分割。
  • v0.5.223〜v0.5.225 ― Phase C1〜C2:ライトバリアランタイムインフラ、codegenがバリアを発行、すべてのヒープストアがそれを通る。
  • v0.5.226〜v0.5.228 ― Phase C3a〜C4:remembered-setのルートがマーク + クリアに流れる;マイナーGCトレースは旧世代をスキップ;非移動式テニュアリング。
  • v0.5.229〜v0.5.236 ― Phase C4b α/β/γ/δ:フォワーディングポインタインフラ、ピン留め + 退避パス、スキャナ + 推移的ピン留め、参照リライト、アイドルナーサリブロックをOSに返却、GCトリガーを初期しきい値で上限。
  • v0.5.237 ― Phase D Part 1:PERRY_GEN_GC=1がデフォルト。
  • v0.5.238 ― Phase D Part 2:PERRY_SHADOW_STACK=1がデフォルト。
  • v0.5.239〜v0.5.240 ― クローズアウトドキュメント:ロードマップ確定、学術 + 産業の系譜の付録(Bartlett 1988、Ungar 1984、Cheney 1970)。

最も重要だった測定された勝利:test_memory_json_churnはgen-GCのデフォルトが切り替わった瞬間に、ピークRSSが115 MB → 91 MBに下がりました。計算面のリグレッションは小さく、言い訳なしに列挙されています ― nested_loops 8 → 18 ms、accumulate 24 → 34 ms、object_create 0 → 1 ms、array_read / array_writeがそれぞれ +1 ms。エスケープハッチ(PERRY_GEN_GC=0)は古い数値を取り戻します;トレードオフは意図的なものであり、ベンチマークページは今や両方の行を並べて掲載しているので、読者が選べます。

2. デフォルトオンのSmall String Optimization

SSOは22バイトのインライン文字列表現で、短い文字列のヒープアロケーションを回避します ― 典型的なJSONキー(2〜8バイト)と短い値はインライン形式に収まります。ロールアウトは表面的には小さく、内部的には大きいものでした:

  • v0.5.213:SSOインフラ(表現 + アクセサ)。
  • v0.5.214:Step 1コンシューマアーム + テスト用PERRY_SSO_FORCEゲート。
  • v0.5.215:Step 1.5 codegen PropertyGet三方向分岐 ― インライン文字列のファストパス、ヒープ文字列のファストパス、残りのスローパス。
  • v0.5.216:Step 2フリップ ― デフォルトでSSOを発行。

v0.5.279のフォローアップは、SSOがホットになって表面化した最後のプロパティリードNaNバグを閉じ、v0.5.272のチェーンされたクロスモジュールゲッターディスパッチ修正はもう1つを閉じました。両方ともデフォルトが切り替わる前のパンチリストにありました;両方ともperfのリグレッションなしに出荷されました。

3. JSON:テープベースパース、デフォルトで遅延

JSONパイプラインはこの期間で最も侵襲的な書き換えを受けました。旧動作:JSON.parseはNaNボックス化された値の完全にマテリアライズされたツリーを構築していました。新動作:JSON.parseは1値あたり12バイトのテープを構築し、遅延でマテリアライズします ― あなたが実際に読んだ値だけがマテリアライズコストを支払います。変更されていないパースに対するstringifyは、今や元の入力のmemcpyになります。simdjsonがraw_json()で使うのと同じファストパスのトリックです。

  • v0.5.200JSON.parse<T>(blob)スキーマ駆動パース(Step 1)。コンパイル時に既知の形状により、コンパイラは事前解決済みのキーアクセスを発行できます。
  • v0.5.203:テープベースパース基盤 ― Step 2 Phase 1。
  • v0.5.204:遅延パース + 遅延ストリンギファイ ― Step 2 Phase 2+4。
  • v0.5.206:遅延セーフなインデックス付きアクセス + エッジケース ― Step 2 Phase 3。
  • v0.5.208:要素ごとの疎なマテリアライズ ― Step 2 Phase 5b。
  • v0.5.209:ウォークカーソル + 適応マテリアライズしきい値。
  • v0.5.210:≥1 KBのブロブで遅延パースをデフォルトにフリップ。

遅延テープが設計対象としていたワークロード(10kレコード、約1 MBのブロブ、中間的反復なしのparse → stringify)における結果:

実装中央値 (ms)p95 (ms)σピークRSS
c++ -O3 -flto (simdjson)24281.28 MB
perry (gen-gc + lazy tape)75916.985 MB
rust serde_json (LTO)1851901.711 MB
bun25934226.182 MB
node39460260.1127 MB
kotlin (kotlinx.serialization)47353321.4606 MB
assemblyscript+json-as (wasmtime)59862110.558 MB

Perryの中央値75 msは比較中で最速の動的型付けランタイムです ― Bun(259 ms)に勝ち、Node(394 ms)に勝ち、KotlinのサーバJIT(453 ms)に勝ちます。simdjsonの24 msはSIMDアクセラレートされたC++の上限であり、つまみ食い(cherry-pick)の裏に隠さず、意図的にページ上に存在します。Perryはそれには勝てません。ポイントはギャップを示すことで、それを閉じることに目標を持たせることです ― docs/json-typed-parse-plan.mdで追跡されています。

正直なコンパニオンベンチはparse-and-iterateです:同じブロブですが、各反復ですべてのレコードのnested.xを合計するので、遅延テープのマテリアライズが強制されます。そこではPerryは466 msに着地します ― マーク・スイープのエスケープハッチの375 msより遅いのは、テープが償却できないオーバーヘッドを支払うからです。その行はTL;DR §Bにあります。作業を回避できないとき、遅延テープは振りをしません。

4. 書き直されたベンチマークページ

Perryがパフォーマンス数値を提示する方法について、3つのことが変わりました。

RUNS=11 中央値 + p95 + σ + min + max、best-of-Nではない。Best-of-Nはテールレイテンシを静かに落とします;このハードウェアでは、9.4秒のPython accumulate外れ値とSwift JSONの5.3秒のp95スパイクを隠していました。中央値はテールをページに戻します。方法論の変更はv0.5.248で着地しました;TL;DR §Aと§Bのすべてのセルは2026-04-25時点でRUNS=11の最新版です。

最適化プローブは本物のランタイムperfから分離されています。Perryが12〜34 msで、Rust/C++が98 msと示している5つのセル ― loop_overheadmath_intensiveaccumulatearray_readarray_write ― はコンパイラフラグの姿勢を測っており、シリコンを測っているのではありません。今は独自のサブセクションにあり、上の段落でclang++ -O3 -ffast-mathがそれらを1ミリ秒以内まで閉じることを説明しています。見出しの本物のランタイムカーネルはloop_data_dependentです:Perry 235 ms、Rust 229、Swift 233、Java 229、Bun 232 ― コンパイラが本当に作業を畳み込めないカーネルで、Perryはno-FMA-contractグループのど真ん中に座っています。それが正直な比較です。

ピアが追加されました。simdjson(4.3.0)は両方のJSONテーブルにあります ― C++のパーススループットの上限で、読者がギャップを見られるようにページ上にあります。AssemblyScriptとjson-as(1.3.2)は最も近いインストール可能なTS-to-nativeのピアです;porfforはこのサイズのワークロードでsegfaultし、Static HermesはmacOS arm64ではインストールできませんでした。Kotlinとkotlinx.serializationはv0.5.241〜v0.5.242でJSONポリグロットに加わりました。すべての行は本物で、すべての免責事項はページ上にあります。

5. ポリグロット計算テーブル

v0.5.249での2026-04-25にリフレッシュされた、本物に折り畳めない見出しカーネル、RUNS=11中央値:

ベンチマークPerryRustC++JavaNodeBun
fibonacci3183303152821022589
loop_data_dependent235229129229322232
object_create1005116
nested_loops1888111821

fibonacciでは、Perryはコンパイル済みグループに3〜15 ms以内で並びます。JavaのHotSpot JITは再帰呼び出しのインライン化により約11%高速です。loop_data_dependentでは、カーネルは2つのFP-contractクラスタに分かれます:約128 msのFMA-contractグループ(Goデフォルト、Apple Clangのg++ -O3 ― どちらもsum * a + bを単一のFMADDDに融合)と、229〜235 msのno-contractグループ(Perry、Rustデフォルト、Swift、-XX:+UseFMAなしのJava、Bun)でスカラFMUL + FADDを実行。LLVMは-ffp-contract=fastでFMAグループに並びます;Perryはそれをデフォルトでは有効にしません。nested_loopsはキャッシュバウンドであり、計算バウンドではありません;全員が8〜21 msに着地します。

6. 軽量なWindowsツールチェーン

WindowsユーザはVisual Studioのインストールが不要になりました。v0.5.199#176を閉じました:perry setup windows + winget LLVM + xwinが、VS BuildToolsツリー全体を置き換えます。v0.5.201find_lld_link / find_perry_windows_sdkのcfgゲートを取り除いたので、パス検出はWindowsをターゲットとするすべてのプラットフォームで機能します ― macOSホストだけではなく。

# Windows host
winget install LLVM.LLVM
perry setup windows
perry compile src/main.ts --target windows -o myapp.exe

7. ランタイム正しさのパス

この期間のテーマ:V8/JSCからの静かなランタイム発散が、修正かコンパイルエラーのいずれかになりました。自明でないものは:

  • v0.5.255BigInt.fromTwos/toTwos 2の補数。
  • v0.5.263Promise.all/race/anyの非Promise型判別。
  • v0.5.281NaN==NaN + ECMAScript数値フォーマット(3 → "3"であって"3.0"ではない;-0 → "0";など)。
  • v0.5.280(x) | 0でのNaN/InfinityのToInt32強制。
  • v0.5.284:Promiseマイクロタスク FIFO + スロー済みハンドラの伝播。
  • v0.5.286:素のf64のJSON.stringifyがテープパスでsegfaultしていました。
  • v0.5.277fs.readFileSyncはエンコーディングが渡されていないときBufferを返します(Nodeに一致)。
  • v0.5.272:チェーンされたクロスモジュールゲッターディスパッチがundefinedを返していました。

Issue #187のstdlibフォローアップが埋まりました:AsyncLocalStorageのエンドツーエンド(v0.5.261)、.action()を実際に呼び出すcommanderランタイム + codegen(v0.5.250)、decimal.jsコード(v0.5.259)、Redis ioredisのエンドツーエンド(v0.5.270)、pg + mongoのasync-factoryパターン(v0.5.275)、そしてEE/LRU/WSSの同じasync-factoryバグ(v0.5.252)。

perry/ui側では:通知タップコールバック(#97)がApple(v0.5.254)とAndroid(v0.5.258)の両方で配線されました;ローカル通知のスケジュール + キャンセル(#96、v0.5.244);AndroidでのFCMの登録 + 受信(v0.5.262)。

8. まとめ

この期間のパターンは見出し数値ではありません。既存の勝利を精査に耐えさせる作業です:持続的なアロケーションワークロードを捕捉する世代別GC、短い文字列のコストギャップを閉じるSSO、最も一般的なワークロードの「変更なし」構造を活用するJSONパイプライン、そしてbest-of-Nではなく中央値を測り、Perryの75 msと同じ行にsimdjsonの24 msのパース上限を表示するベンチマークページ。読者はギャップを見ることができます ― そしてPerryが床に対してどこに座るかを。

試してみてください:

# npm (any platform)
npm install @perryts/perry
npx perry compile src/main.ts -o myapp && ./myapp

# Homebrew (macOS)
brew install PerryTS/perry/perry

# winget (Windows — no VS install needed)
winget install PerryTS.Perry

# Default benchmark suite
cd benchmarks/json_polyglot && ./run.sh
cd benchmarks/polyglot && ./run_all.sh

ソース:github.com/PerryTS/perry ― ベンチマーク:benchmarks/README.md ― チェンジログ:CHANGELOG.md

― Ralph