モチベーション
この記事は GRIPHONE Advent Calendar 2021 23日目の記事です。
つい先日、私が関わっているプロジェクトではphp7からphp8へとバージョンアップを行いました。そういった中で、php8のJITの導入背景が理解できていないことに気づきました。
この記事ではJITの動作や導入背景からphp8と7の違いを理解することを目指します。
インタプリタとJITとは?
PHPインタプリタ(Zend Engine)
PHPはインタプリタ形式の言語です。phpインタプリタはソースコードを実行時に逐次解釈しながら実行します。より具体的にはプログラムは以下のように解釈され、実行されていきます。
- PHPのソースコードを解釈して、トークン列に変換する
- 構文規則に従ったトークン列であるか解析し、抽象構文木を生成する
- 抽象構文木をバイトコードに変換する
- バイトコードをVM上で実行する
バイトコードとは実行環境に依存しない中間形式(IR)であり、phpではopcodeと言います。
翻訳が終わればバイトコードは破棄されます。バイトコードが破棄されてしまうので同じリクエストがあってもプログラムを解析し直す必要があります。
PHPの最適化
インタプリタ形式の言語を高速化する手法は例えば次のような手法です。
- アクセラレータ バイトコードをメモリに保存しておいて、次のリクエストで呼び出す。
- JIT 実行時にバイトコードをネイティブコードに変換する。
phpのアクセラレータをOPcacheと言います。アクセラレータを使うと、ソースコードを解釈してバイトコードに変換するまでの処理をスキップできます。
PHP7.4以前ではJITを採用せず、アクセラレータやメモリ周りの改善による効率化が行われていました。
JIT
JITとは、プログラムの実行中で必要なときだけコードをコンパイルする仕組みです。JITコンパイラを採用すると次のような利点があります。
- ネイティブコードで実行するので効率が良い
- 中間形式を経由することで最適化が施せる
しかし、ネイティブコードはCPUに依存し、実行環境によって異なるネイティブコードが必要になるので、言語処理系のバグや開発、保守のコストが増加する欠点があります。
PHP8のJIT
2020年11月27日に公開されたPHP8は約5年ぶりのメジャーアップデートということで次のような機能改善が行われました。
- matchやunionなどの新機能
- 比較演算・エラー処理の改善
- JITコンパイラの採用
この中で特に注目度が高いのがJITコンパイラの採用です。
JIT採用の背景
PHPはJIT無しでの性能向上が望めなくなり、PHP8ではついにJITが採用されました。JITコンパイラはOPcacheの一部として実装されており、必要な場合はphp.ini
でオプトインする必要があります。
PHP7でJITが採用されなかったのには以下のような背景があります。
- phpの最適化戦略がまだ存在していた(データ構造やキャッシュの見直し )
- 前述のような開発・保守の複雑化
- 1.の最適化戦略なしにはJITでの高速化が望めなかった
1は例えば
- PHPの変数(zval)に存在した未使用メモリをなくす事によって、高速なキャッシュを使いやすくした 。
- 配列の要素がメモリ上で連続して配置されるようにした。
といった戦略です。このあたりの背景はPHPNG(PHP7)のRFC に詳しく書いてあります。
JIT導入による変化
PHP8においてJITを有効にすると次のように動作します。
- PHPのソースコードを解釈して、トークン列に変換する
- 構文規則に従ったトークン列であるか解析し、抽象構文木を生成する
- 抽象構文木をバイトコードに変換する
- バイトコードをVM上で実行する。またはJITコンパイラの適用単位に応じてネイティブコードにコンパイルして実行する。
OPcacheだと同じようなリクエストが来た時に、キャッシュしておいたバイトコードを使い回します。
JITではネイティブコードを使い回すことができます。ネイティブコードを使いまわせる分、JITの方が効率が良くなります。
JITのモード
php8ではJITを行う単位が2種類存在します。
- Tracing JIT
- Function JIT
二つの単位はopahce.jitによって切り替えることが可能です。違いは次のようになります。
opcache.jitの指定 | JITを使う単位 | 備考 | |
---|---|---|---|
Tracing JIT | tracing/on | ホットコード | opcache.jitのデフォルト値 |
Function JIT | function | 関数 |
より具体的には
- Tracing JITではプロファイリングでホットコードを検出し、そのホットコード(より正確にはホットコードで行われた全ての演算)がコンパイルされます。
- Fuction JITでは全ての関数をコンパイルします。
まとめ
この記事ではJIT導入の背景からphp8のインタプリタ動作についてまとめました。PHP8への移行による性能の変化ついて詳しくみたい方はバックエンドエンジニアの森さんの記事を参考にしてみてください。
参考
PHP RFC: Move the phpng branch into master
https://wiki.php.net/rfc/phpng
https://qiita.com/rana_kualu/items/ba312d2789bd228f887a