コンテンツへスキップ

ベンチマーク

Phasisは、ディスパッチ負荷の高いJavaScriptでV8よりおよそ2桁遅くなります。この限界は、ネイティブJITコンパイラーに対するPHPホスト型インタープリターに本質的に伴うコストです。組み込み側が考えるべきなのは「V8と比べてどうか」ではなく、「自分の用途に十分な速さか」です。多くの組み込み用途では、答えは「はい」です。

この限界が問題にならない場合

  • テンプレート処理 — 一般的なページのSSR形式のレンダリングは10 msを大幅に下回ります。
  • 検証ルール — ユーザーが指定したJSによるフォーム検証はマイクロ秒で完了します。
  • データ変換 — 数百件に対するarray.map(transform)は数十ミリ秒で完了します。
  • サンドボックス実行 — 実行時間をホスト側で1秒に制限した短いユーザースクリプトなら、十分な余裕があります。

この限界が問題になる場合

  • 計算負荷の高い内側ループ — 画像処理、大きな配列の縮約、シミュレーションなどです。計算処理をネイティブのPHPライブラリーに移し、ホスト関数として公開してください。
  • リアルタイムアニメーション — 60 fpsで動作するものはすべて対象です。そのJSは組み込みエンジンではなく、クライアント(ブラウザー、ネイティブアプリ)で実行してください。
  • 計算負荷の高いアルゴリズムのJSによる再実装 — 暗号化、圧縮、解析などです。PHPには1000×高速なネイティブ版があるため、そちらを使ってください。

適切な捉え方は、PhasisをPHP形式のアプリケーションに埋め込む、JS形式のロジックをつなぐ糊付け層と考えることです。計算処理はPHPホストが担い、JS層は設定、分岐、小規模な変換を担います。

素朴なツリーウォーカーと比べてエンジンが高速な理由

  • バイトコードVM — ホットなディスパッチの大半はASTではなく、VM::executeのopcodeスイッチを通ります。
  • カスタム呼び出しスタック — すべてのJS呼び出しの状態をVM::execute内に保持し、呼び出し元のフレームをプールに保存します。JS呼び出しごとに新しいPHPフレームを作りません。詳細はバイトコードVM § カスタム呼び出しスタックを参照してください。
  • インラインキャッシュ — PCごとの単相キャッシュにより、プロトタイプメソッドをインスタンスから読み取る際(c.method()の形)、プロトタイプチェーンの走査を省略できます。
  • フレームスナップショットジェネレーター — 単純なジェネレーター本体は、PHP Fiberを経由せず、ディスパッチ状態をヒープオブジェクトに保存して中断します。
  • JsToPhpトランスパイラー — 数値処理やローカル変数中心の本体(obj.method()、new Ctor()、this.memberの読み取りを含む)はPHPクロージャーにコンパイルされ、PHP 8.5のトレーシングJITによってネイティブマシンコードに低レベル化されます。
  • JSON.stringifyキャッシュ — JSON.stringify(o)は推移的なバージョン無効化を行い、入力オブジェクトに出力をキャッシュするため、JSON.parse(JSON.stringify(o))というディープクローンの慣用表現を高速化します。

独自のワークロードを測定する

シェル
php bench/run.php

これはbench/microbench.jsに含まれるマイクロベンチマークスイートを実行します。ループ演算、再帰関数、オブジェクト生成、プロトタイプメソッド呼び出し、配列への追加、JSONの往復変換など、いくつかの合成ワークロードを測定し、各テストの実行時間の中央値を出力します。動作確認に便利です。現在の数値は、各CIベンチマーク実行時にBENCH.mdへコミットされます。

独自のコードを測定するには、任意のスクリプトに置き換えてください。

シェル
php -d xdebug.mode=off bench/run.php path/to/your-script.js

PHPレベルのプロファイリングには、XdebugのプロファイラーをPhasisに対して、他のPHPアプリケーションと同じように使えます。xdebug.mode=profileでスクリプトを実行し、cachegrindの出力を調べて、どのPhasis\クラスが処理時間の大半を占めるか確認してください。

ロードマップ

実現の容易さをおおまかな順に並べると、V8との差をさらに縮めるには次の対応が考えられます。

  • 多相インラインキャッシュ — 1つのLOAD_MEMBER呼び出し箇所で、低速なディスパッチに切り替わる前に2〜4個の形状をキャッシュできるようにします。異なるレシーバーを処理するコードに効果があります。
  • yield*と、フレームスナップショットジェネレーター経路でのtry-with-yield — 現状では、これらの形はFiberベースのツリーウォーカーに送られます。スナップショットに対応させるには、明示的な状態機械での処理(委譲イテレーターの転送、中断中もハンドラースタックを保持)がそれぞれ必要です。
  • 非同期ジェネレーターとスナップショット上のawait — 仕様では、ジェネレーターの中断とマイクロタスクキューが組み合わさります。バイトコード本体がyieldとawaitの両方に対応すれば、非同期ジェネレーターもスナップショットに対応できます。
  • JsToPhpでのパラメーター型推論 — 関数パラメーターは既定で数値型となるため、function (obj) { obj.method() }ではコンパイルが中断されます。本体内での使われ方からオブジェクト型のパラメーターを推論できれば、newでレシーバーをローカルに生成しなくても、関数引数のメソッドを呼び出せるようになります。
  • opcacheへの直接出力 — opcache形式のopcodeを直接書き込み、JsToPhpのeval()段階を省略します。最も大がかりな項目ですが、ほかの対応は既存のアーキテクチャ内で実現できます。

これらはどれも、Phasisが対象とする組み込み用途には必要ありません。