Porffor: ゼロから作られた実験的な Ahead-of-Time JS エンジン
(porffor.dev)- Porfforは、JavaScriptを実行時ではなく事前にコンパイルして WebAssembly とネイティブバイナリ に変換する研究プロジェクト
- インタープリタを同梱しない方式により、既存の JS→Wasm プロジェクトより出力物が 10〜30倍小さく高速 であることを目指す
- ネイティブビルドでもランタイムをパッケージしないため、バイナリサイズが最大 1000倍 小さくなる可能性があり、例では約 90MB から 100KB 未満まで減少
- JS で書かれており
evalはなく、TypeScript を別ビルド段階なしで ネイティブサポート する構造を掲げる - AOT は静的解析の最適化と実行前コンパイルに有利だが、
evalのような動的な JS 評価が難しく、まだ多くの JS が動作しない初期段階
Porfforが実現する実行方式
- Porfforは、JavaScriptを WebAssembly と ネイティブバイナリ に Ahead-of-Time コンパイルする研究プロジェクト
- Porfforでコンパイルされた TypeScript バイナリ がこのページを提供している
- 最初から AOT を念頭に置いて作られており、従来の JS 実行方式では難しかった最適化を試みる構造
WebAssembly とネイティブ出力の違い
-
JS → Wasm
- Porfforの WebAssembly 出力は、既存の JS→Wasm プロジェクトより 10〜30倍小さく高速
- 中核となる違いは、JS を直接コンパイルし、インタープリタをバンドルしない点にある
- JS を Wasm 上で実行するとサンドボックス実行が可能になる一方で大きな性能損失が生じることがあり、Porffor はこのコストを減らすことに重点を置く
- 適用可能な事例:
- サーバーサイド JS ホスティング: エッジランタイムで Wasm サンドボックスにより、過剰な分離なしに安全な実行を提供できる
- AOT の低いオーバーヘッドは、JIT と比べて同じハードウェア上で、最小限の性能低下でもっと多くの顧客を実行できる可能性を生む
- リバースエンジニアリング耐性: 機密性の高い JS は、難読化よりもコンパイル済みコードのほうがリバースエンジニアリングしにくい場合がある
-
JS → Native
- ランタイムをパッケージせずに JS を実際にコンパイルするため、バイナリサイズが最大 1000倍 小さくなる可能性がある
- 例のサイズは約 90MB → 100KB 未満
- 内部的には JS を C にコンパイルした後でネイティブにコンパイルするため、C を使える場所では JS を使える
- 適用可能な事例:
- 組み込み、ゲームコンソールなどでの 高速な JS 実行
- 1MB 未満のワンクリック実行ファイルにコンパイルされる 小さな JS CLI アプリ
AOT がもたらす利点と制約
- 従来のインタープリタや複数の JIT 段階は、起動時間と JS 性能のあいだでバランスを取る必要がある
- AOT は先にコンパイルして後から実行するため、コンパイル速度は開発者体験には重要だが、ユーザー体験には影響しない
- この方式は、C++ や Rust のような 静的解析ベースの最適化 を行う余地を与える
- 主な欠点は、
evalのような動的な JS 評価がなく、JS エンジンを新たに作る必要がある点 - まだ初期段階のため多くの JS は動作しないが、改善作業が進められている
- ECMAScript 互換性の進捗を追跡するため、コミットごとに公式テストスイートである Test262 を実行している
1件のコメント
Hacker News のコメント
Porffor の主開発者である Oliver が、Porffor にフルタイムで取り組むと発表: https://x.com/canadahonk/status/1818347311417938237
https://news.ycombinator.com/user?id=defunkt
似たようなものを考えたことはあるが、JavaScript で大幅に高い性能を出すのは難しいと思う。おそらく最善でも、JS を V8 の C++ 呼び出しへトランスパイルする程度だろう
本当にすごい最適化は、TypeScript かそれに近いものをコンパイルするときに出てくる。型を活用すれば大きな利得が得られ、型がない部分は基本的に遅い JS 呼び出しに落ちることになる。インターフェースは仮想関数テーブルや直接呼び出しに縮約でき、マップの代わりに構造体上で動作させることもできる。
IntとFloat型を用意し、必要なときにNumberへ下げつつ、レジスタ内に置いておくこともできる核心的な問題は、TS と V8 がどちらも急速に変化する非標準の対象である点だ。こうしたプロジェクトには大きなチームが必要で、互換性の維持そのものが別の仕事になる
簡単な例として、TypeScript は整数と浮動小数点数を区別せず、すべて数値として扱う。そのため、すべての配列アクセスで型変換が必要になる。静的コンパイルを助けるよう設計された TypeScript なら、おそらくこの区別があったはずだ
より大きな問題は、TypeScript の 構造的部分型だ。この性質のため、コンパイラが関数に渡される非プリミティブ引数の物理的な構造を静的に決定することは事実上不可能になる。JIT は動的な形状解析ができるため、すべてのフィールドアクセスで JIT より悪い性能になる可能性がある
JS 向けの静的型解析ツールを作る取り組みは多くあり、非常に徹底した解析も可能だ。思い浮かぶ例としては少し古いが TAJS がある
TypeScript でも
integerのような型を指定できるとよい。通常の TS→JS コンパイルではconst val: intをconst val: numberとまったく同じに扱うとしても、TS を認識する最新のランタイムならその追加情報を活用できるはずだconst counter: Numberのような構文が受け入れられるのか気になるサイトが変わったのか、それとも自分が何か見落としているのか分からない
windmill.dev では、ユーザーがコードをデプロイする際に Bun build を使ってスクリプトとすべての依存関係を単一の JS ファイルにまとめ、それをロードしてコールドスタートとメモリ使用量を改善している。バンドルサイズのため、成果物は S3 に保存している
すべてをネイティブにバンドルできるなら、状況はまったく変わる。Bun のコールドスタートがどれほど優れていても、小さなバイナリでそのままネイティブ実行するものには勝ちにくい
Wasm にアプローチする JS ランタイムが増えているのは良いことだと思う。このプロジェクトは、React Native プロジェクトの iOS・Android での速度を高めるための Facebook の JS エンジンである Static Hermes を思い起こさせる。
どちらも JS test262 準拠を目標にしており、Porffor はネイティブ出力と Wasm 出力の両方をサポートする一方、Static Hermes は現在、主にネイティブ出力に注力している。Porffor は純粋な JS で書かれていて、自分自身をコンパイルできる方向を目指しており、Static Hermes は LLVM に依存している。Porffor は async/promise/await のサポートがまだ限定的で、Static Hermes は一部制限つきでサポートしている。Static Hermes は C++ で、Porffor は主に JS で書かれている。どちらも TypeScript をサポートするが、Static Hermes は TS AST を Flow にトランスパイルし、Porffor はネイティブにサポートしている。Static Hermes には
evalのようなコンパイルが難しい JS の状況のための代替インタプリタがあり、Porffor は事前コンパイルのみをサポートする。全体として、このプロジェクトが勢いを得て エッジの JavaScript エンジンをさらに高速化できるか期待している。Wasmer の Syrus として投稿。
https://github.com/facebook/hermes/discussions/1137
https://github.com/tc39/test262
https://wasmer.io
ただし私たちの焦点ではなく、主に React Native に集中している。その環境では WASM にはあまり意味がない。
Static Hermes の最も重要な機能は、ランタイムの健全性を保証する型チェッカーだ。Porffor は非常に興味深く、しばらく見守ってきたし、うまくいってほしいと思っている。
Kiesel と似たアプローチだ: https://kiesel.dev/
Array.prototype.filter、Math.sin、atobのような組み込み機能は、部分的に自分自身をコンパイルしている。最近は Porffor も基本的な async/promise/await をサポートし始めた。まだそれほど良くはない。
JavaScript には簡単にコンパイルできるサブセットがあり、難しいのはその外側にある長いロングテールだ。それでも、その 境界がどこにあるのか、そしてそのサブセットでどれだけ得をできるのかの研究が進んでいるのは素晴らしい。
String.blinkをサポートしている点が本当に気に入った。開発者に ユーモアと遊び心があるのは、いつでも良い兆候だ。実装も
function() { return "" + this + ""; }程度の些細なものなので、ECMAScript ホストが Web ブラウザでなくても実装する価値はある。その場合は任意だ。これが「ユーモアや遊び心」と関係しているとは期待していない。String.blinkは test262 に入っているので、プロジェクトの目標を達成するには実質的にサポートする必要がある。自分が見落としている微妙な違いが何なのか気になる。なぜ「事前コンパイル JS エンジン」が「JS-to-Wasm コンパイラ」より良い説明なのか分からない。主にフレーミング戦略なら、それはそれで構わない。
ここで説明されている バージョン体系は少し疑わしい。
ある変更によって一部の Test262 テストにリグレッションが生じると、バージョン番号も一緒に戻る可能性がある。つまり Porffor は、単調増加するバージョン番号と、必要な変更が Test262 のリグレッションを引き起こし得る能力を同時には持てない。
https://github.com/CanadaHonk/porffor?tab=readme-ov-file#ver...
ウェールズ語で「紫色」という意味だ。
さまざまな用途の JS エンジンがいくつも出てきているのを見るのは新鮮だ。
アプリケーションにプラグインを組み込むため、llrt を通じて quickjs に Node 互換 API をさらに提供する作業をしてきた。
https://github.com/awslabs/llrt