なぜ TypeScript に LLVM なのか?
事前(AOT)コンパイラは JIT とはまったく異なる領域で生きて います。JIT はユーザーが待っている間にコンパイルするため、 コンパイルのレイテンシそのものが制約になります。Perry のよう な AOT コンパイラは、開発者のマシン上や CI 上で一度だけ コンパイルし、そのバイナリはその後何百万回も実行されます。 この非対称性こそ、重量級のオプティマイザが元を取れる場所です。
LLVM は20年分のミドルエンドの成果をもたらします:ループの ベクトル化、ループ不変コードの移動、グローバル値番号付け、 疎な条件付き定数伝播、積極的なインライン化、エイリアス解析。 Perry の仕事は、実際に最適化できる IR をその機構に渡すこと です——ここで TypeScript の型情報が生きてきます。
ローワリングパイプライン
ソースは SWC でパースされたのち、型付きの高レベル IR(HIR) へと下げられます。興味深い決定はすべて、LLVM がコードを目に する前にここで行われます。
- 単相化。 ジェネリックな関数やクラスは、具体的なインスタンス化ごとに 特殊化されます。Rust や C++ が使うのと同じ戦略です。
Stack<number>とStack<string>は2つの独立した完全に型付けされた関数になります——そのため オプティマイザは汎用のディスパッチの塊ではなく具体的な型を 扱うことになり、ジェネリクスは実行時に一切コストがかかり ません。 - 静的ディスパッチ。 レシーバーの型がコンパイル時に判明している場合、メソッド 呼び出しはハッシュテーブルの参照ではなく、LLVM がインライン 化できる直接呼び出しにコンパイルされます。
- 直接的なフィールド アクセス。 オブジェクトのフィールドはコンパイル時のインデックスに解決 されるため、プロパティの読み取りは辞書の参照ではなく固定 オフセットのロードになります。
NaN-boxing とインラインローワリング
値が動的な場合、Perry は NaN-boxing を使用します:すべての値 は64ビットのワードです。倍精度浮動小数点数はそのまま格納され、 オブジェクト、文字列、真偽値、null、undefined は IEEE 754 の quiet NaN の未使用ビットパターンにエンコード されます。数値はゼロコストです——ボクシングも、算術のための アロケーションも発生しません。
問題は、数値以外の値に対する操作は unpack-operate-repack の ビット列を必要とすることです。これらの列が別途コンパイルされ たランタイムへの呼び出しとして存在していると、LLVM からは 不透明なブラックボックスに見え、その内側をまたいで最適化する ことができません。そこで Perry は、プロパティ読み取り、 メソッドディスパッチ、オブジェクト割り当てといったホットな 操作を、オプティマイザが融合・単純化できるインラインの LLVM IR として出力します。たとえばオブジェクト割り当ては、インライン のスレッドローカルなバンプアロケーションへとコンパイルされ ます。
%off_ptr = getelementptr i8, ptr %state, i64 8
%offset = load i64, ptr %off_ptr ; current bump offset
%new_off = add i64 %offset, 96 ; headers + 8 fields
%sz_ptr = getelementptr i8, ptr %state, i64 16
%size = load i64, ptr %sz_ptr ; block capacity
%fits = icmp ule i64 %new_off, %size
br i1 %fits, label %fast, label %slowなぜ Cranelift ではないのか?
Perry の最初のバックエンドは Cranelift でした——wasmtime を 支えるコード生成基盤で、高速かつ予測可能なコンパイルのために 作られています。出発点としては正しい選択であり、JIT やサンド ボックス化されたランタイムにとっては今も優れた選択肢です。 2つの要因が乗り換えを迫りました。
- オプティマイザの天井。 Cranelift は意図的に高速な単一ティアのコンパイラです: 「まともなコードを素早く」というのは、JIT にとっては正しい トレードオフであり、ネイティブパフォーマンスを売りにする AOT コンパイラにとっては誤ったトレードオフです。
- arm64_32。 Apple Watch は Cranelift がサポートしていない ABI (64ビット命令、32ビットポインタ)を使用しています。 watchOS をターゲットとして成立させるには LLVM が必要で あり、2つのバックエンドを維持することは2組のバグ、テスト、 パフォーマンス基準を維持することを意味しました。
この移行は無償ではありませんでした。ホットな操作が当初は 不透明なランタイムヘルパー呼び出し経由になっていたため、 最初の LLVM 専用リリースは一部のベンチマークで最大70倍も 後退しました。そこから回復する過程——インラインローワリング、 上記のバンプアロケータ、より良いインライン化の境界——で バックエンドは Cranelift の数値を超え、落ち着いた頃には Perry はスイート内のすべてのベンチマークで Node.js を1.7倍 から24.6倍上回り、2つのタイがありました(2026年4月)。 この後日談は一読の価値があります:From Cranelift to LLVM。
さらに詳しく
コンパイラ内部構造のページ では、NaN-boxing、単相化、静的ディスパッチをさらに詳しく解説 しています。ブログでは、Optimizing Everything がリリースごとの最適化作業を追いかけており、Gen GC, lazy JSON, and defensible benchmarks ではベンチマーク手法(RUNS=11、中央値 + p95)について説明して います。全体像をつかむには、TypeScript ネイティブコンパイラ の概要から始めてください。