その威力は絶大であり、単純な計算問題であればJITを有効にするだけで1分の処理が8秒になるほどの高速化でした。
そんなJITですが、次のバージョンでさらにブラッシュアップされることになりました。
(PHP8.4としていますが、もしかしたらPHP9.0かも)
以下は該当のRFC、A new JIT implementation based on IR Frameworkの紹介です。
なおCPUとかIRとかさっぱりわからないので、誤りや認識不足が多々含まれています。
誰かがPRで修正してくれるはず。
PHP RFC: A new JIT implementation based on IR Framework
Introduction
PHP用JITの最初のバージョンはPHP8.0でリリースされました。
その際、我々は最も単純なアプローチを採用しました。
すなわち、Zend VMのバイトコードから直接ネイティブコードを生成するというものです。
このアプローチによってJITを短期間で提供することに成功しましたが、同時に最適化という観点において大きな制約が発生しました。
スマートな最適化には、より詳細な中間表現 ( IR ) が必要です。
またPHP8.1でAArch64用のJITバックエンドを導入したため、JITのメンテナンスが少し複雑になりました。
というのも、2種類のバックエンドでアセンブラのコードを更新しなければならなくなったからです。
中間表現を経由するスマートなJITコンパイラについて、長期的計画を練ってきました。
このProposalは、2年弱にわたる作業の結果です。
Proposal
IR Frameworkをベースにした新しいJITを実装します。
x86とAArch64それぞれのコードを生成する複数のバックエンドのかわりに、IRを構築する単一のバックエンドを実現します。
できたIRは、マシンに依存しない最適化、レジスタ割り当て、スケジューリング、ネイティブコード生成等を行うフレームワークに渡されます。
新しいIRでは、レジスタ割り当てや呼び出し規約など、ローレベルで差異がある部分を気にする必要がなくなります。
その一方で、全ての依存関係を一貫して反映する必要があるため、IRの構築が複雑になる可能性があります。
これはIRの検証中にエラーを発見する際に役立ちます。
新しいJITの実装が影響するのは、JITの開発保守に関わっている、ごく一部のPHP本体の開発者だけです。
JITは独立したサブシステムであり、その内部の変更が外部に影響を与えることはありません。
本RFCのパッチはext/opcache/jit
の外側はほとんど変更しておらず、また新旧バックエンドで動作が互換するよう注意深く実装を追加しています。
現在のところ、本RFCではこれまでのJIT実装を削除していません。
これまでのJIT実装を使い続けたい場合は--disable-opcache-jit-ir
オプションで切り替えることができます。
JITは非常に複雑なサブシステムです。
現在の実装にはおそらくバグがありますが、対処できることを確信しています。
このRFCが早く受理され早くマージされれば、それだけバグに対応できる期間が増えます。
IRフレームワークを独立して開発する目的のひとつは、外のコンパイラエキスパートとのコラボレーションです。
IRプロジェクトをPHP以外の世界にも持ち出し、専門知識の共有や、コントリビューションを行い、外の開発者をも巻き込むために力を尽くします。
これがうまくいけば、PHPのバス係数を底上げすることにも繋がります。
IRフレームワークの必要な部分はPHPのソースに組み込まれており、新たな外部依存はありません。
IRフレームワークは非常に複雑です。
こちらのプレゼンテーションでは、設計のアイデア、および最も重要な実装の詳細を解説しています。
Backward Incompatible Changes
後方互換性のない変更はありません。
Proposed PHP Version(s)
次のバージョン。
おそらくPHP8.4。
RFC Impact
RFCの影響。
Performance
新しいJITは、現在のものと全く同じ機能と最適化を実装しています。
IRフレームワークはより多くの最適化と、よりスマートなレジスタ割り付けアルゴリズムを持っているので、JITは少し早く (5から10%)、少し小さなコードを生成します。
これはbench.php
やmicro_bench.php
では目に見えますが、実際のアプリケーション速度にはほとんど影響しないでしょう。
Tracing JITのコンパイル速度はほぼ変わりません。
Function JITのコンパイル速度は最大で4倍遅くなります。
これらはJITコンパイラとしてはとても良い数値です。
手元のテストでは、秒間15メガバイト程度のネイティブコードを生成する性能がありました。
Zend VM
サポートされているプラットフォーム ( x86, x86_64, AArch64 ) において、HYBRID VMインタプリタはexecute_ex()
の開始時と終了時、CPUレジスタに保存とリストアを行います。
これによって、JIT化された各関数やトレースを毎回保存・リストアするコードを省くことができます。
CALL VMを使用しているCLANGやMSVCには影響しません。
To Opcache
JITはOpcacheの一部として実装されているため、Opcacheのビルドプロセスが変更されます。
それ以外の影響はありません。
php.ini Defaults
php.ini
に対して新しいディレクティブの追加やデフォルト値の変更はありません。
opcache.jit_debug
ディレクティブに、IR関連のデバッグ機能が追加されます。
・0x01000000 - 構築完了後のIRグラフを表示
・0x02000000 - コード生成直前の最終的なIRグラフを表示
・0x04000000 - 制御フローを表示
・0x08000000 - 割り当てレジスタの情報を表示
・0x10000000 - Sparse Conditional Constants Propagation後のIRグラフを表示
・0x20000000 - Global Code Motion後のIRグラフを表示
・0x40000000 - Register Allocation後のIRグラフを表示
・0x80000000 - IRグラフにコード生成ルール等の注釈を表示
To SAPIs
SAPIへの影響はありません。
To Existing Extensions
エクステンションへの影響はありません。
New Constants
新しい定数はありません。
Open Issues
古いJIT実装はしばらく残しておくか、それとも早々に置き換えてしまうべきだろうか。
Unaffected PHP Functionality
JITは独立したサブシステムとして開発されているため、他の機能に直接影響することはありません。
Future Scope
この項目は将来の展望であり、このRFCには含まれていません。
IRを導入することで、より強力な最適化の扉が開かれました。
これにはPHPに依存しないものも含まれます。
たとえばループの最適化、LOAD/STOREの挙動の改善、コードジェネレータの改善などです。
PHPとはほぼ関係なく、新しいJITのターゲット ( たとえばRISC-V ) をサポートすることが可能です。
Proposed Voting Choices
投票期間は2023/10/06から2023/10/19です。
2023/10/16時点では賛成24反対0であり、まず間違いなく受理されます。
また古い実装を残しておくかどうかの投票も同時に行われ、削除するほうに決まりました。
Patches and Tests
https://github.com/php/php-src/pull/12079
感想
現在のJITは、PHP → opcode → ネイティブコードという変換になっています。
今回のRFCでは、これをPHP → opcode → IRコード → ネイティブコードというふうに一段階増やします。
こうなるに至った経緯や技術的裏付けについては本人が資料を公開されていますが……全く理解できん!
左が現在のJIT、右が変更後のJITです。
右下のバックエンド部分が、これまでCPUごとに実装が必要だったものが、ひとつで済むようになりました。
そのかわりに中間表現が一回増えてるから遅くなるのでは?と思いきや、どうやらこのIRコード → ネイティブコードの部分はPHPだけではなく他の言語でも使うことができて既存の技術を使用できるということらしいです(わかってない)。
そのおかげもあって、結果として全体的な速度も1割弱向上しているようです。
いやはや凄いですね。
ちなみにRFC中にもバス係数という表現が出てくるのですが、現在のJITのバス係数は『1』です。
すなわち、Dmitry Stogovがバスに轢かれたらJITは終わりと言って過言ではありません。
しかしそれも、今回の変更でPHP以外の言語開発者との共同開発の可能性が出てきたことによって、バス係数の向上が見込まれるということです。
とまあ、あまりにいいこと尽くめすぎて本当なのかよって感じですね。
なんにせよ、PHPはまだまだ進化し続けます。
すばらしいことですね。