PHPの最新状況: PHP 8.4ついにリリース (第40回)

PHP

廣川類

 PHP 8.4の開発は順調に進み、計画通り11月21日に一般公開されました。本連載では、PHP 8.4における主な機能強化点について紹介してきました。今回の記事では、PHP 8.4で強化されたポイントのうち、JIT機能の更新と遅延オブジェクトについて紹介します。

中間表現(IR)にもとづくJIT機能の更新

Webアプリケーションでは、ユーザのリクエストに対して高速に応答することが求められており、PHPスクリプトを高速に実行する必要があります。PHPでは、8.0以降でJIT (Just-in-Time)コンパイラを導入し、PHPスクリプトの実行時間の高速化を図っています。 PHPにおけるJITコンパイラの動作の概略を図1に示します。PHPスクリプトは、PHPのスクリプトエンジンによりCPUに依存しない仮想マシン上のバイトコード(オペコード)に変換され、実行されます。一般に、そのバイトコードの実行速度は、x86のようなネーティブCPU上で実行される速度に比べて遅いため、特に複雑なアルゴリズムを実行する際の実行速度はネーティブCPUで直接実行可能なプログラムと比べて遅くなってしまいます。

PHP 8.0で導入されたJITコンパイラは、図1左に示すように、PHPスクリプトから変換されたバイトコード(オペコード)をJITコンパイラによりネーティブCPUで直接実行可能なコードにコンパイルします。JITコンパイラには、スクリプト言語Rua用に作成されたDynASMを用います。

JITを用いる際の実行時間は、JITコンパイラによるコンパイル時間とコンパイルされたネーティブコードの実行時間の和となります。JITコンパイラは、通常のコンパイラと異なり、ネーティブコードの実行時間が少し遅い、つまり、コンパイルによる最適化のレベルが少し劣っていたとしても、コンパイル時間が少ないことが求められます。 PHP 8.0で導入された Tracing JITモードでは、プログラムのコードの中で実行時間がかかる部分を分析し、その部分を中心にネーティブコードへのコンパイルを行います。これにより、コンパイル時間と実行時間の和を最小化することを試みます。

図1 JITコンパイラの更新
図1 JITコンパイラの更新

 しかし、PHPオペコードを特定のCPUのネーティブコードに直接変換するPHP 8.0以降のJITの方式では、JITコンパイラのメンテナンス性を低下させ、結果として最適化が十分行われない可能性がありました。このため、PHP 8.4以降では、図1右のように特定のCPUに依存しない中間コード(IR)にまず変換した上でコンパイラによりネーティブコードに変換する新しい方式が採用されました。中間コード(IR)を処理するフレームワークは、PHP自体に依存しないオープンソースソフトウエアとしてgithubで公開されています。

中間コード(IR)に基づくJITコンパイラの導入により、CPUに依存しない部分と依存することを明確に分離することができるようになり、JITコンパイラによる最適化の見通しが良くなるとともに、RISC-Vのような新しいCPUをサポートすることも容易になりました。

中間コード(IR)に基づく新方式の欠点としては、Tracing JITではなく、Function JITをJIT実行モードとして指定した場合に実行速度が低下することです。なお、通常用いられるTracing JITでは速度の低下はほとんど起きません。

 それでは、実行時間を調べてみましょう。PHPには、ベンチマークツールが2種類(bench.phpおよびmicro_bench.php)付属しています。図2にマンデルブロー集合のように主にアルゴリズム系の処理を扱うbench.phpの実行時間[秒]を示します。この棒グラフは、小さい方が高速であることを示します。左の6つのケースは、JITコンパイラ及びキャッシュを使用しないケースを示します。その右の2ケースはOPcacheによるキャッシュを利用するケース、その更に右の5ケースはJITを利用するケースを示しています。JITコンパイラ及びキャッシュを利用しないケースでは、PHP 7.4と比べてPHP 8.x では15%程度の改善が確認できますが、PHP 8.xの各バージョンの差異はあまり認められません。キャッシュを利用した場合は、概ね実行速度が2倍弱に改善しています。PHP 8.x でJITコンパイラを利用した場合は更に3倍程度実行速度が改善しています。しかし、同じくPHP 8.xの各バージョンでは実行速度に目立った差異は見られません。

図2 ベンチマーク用スクリプトZend/bench.php の実行時間

 変数への代入のように主にスクリプト言語の基本的な機能に関するベンチマークであるmicro_bench.phpの実行結果を図3に示します。この結果も図2と同様の傾向が見られます。JITを使用したケースの結果の中で、PHP 8.4において約18%の改善が見られます。内訳を確認すると、self:$x に関する4つのケースで実行時間が約半分と大幅に改善されており、結果として実行時間にも優位な改善が見られます。

図3 ベンチマーク用スクリプトZend/micro_bench.php の実行時間

 ただし、Webアプリケーションとしての実行時間は、ファイルの入出力やデータベースアクセスなどの他の要素も関係しており、この程度の差では体感できるレベルの改善は見込めません。しかし、IRフレームワークとしてJITコンパイラの処理が明確に分離されたことで、最適化を行うことが容易となり、より高度な最適化の導入による高速化が期待されます。

 なお、PHP 8.4ではJITコンパイラを有効にするためのphp.iniにおける設定も変更となります。JIT機能を使うには、OPcacheエクステンションを読み込んで、有効にする必要があります。その際には、設定ファイルphp.iniに以下の設定を追加します。

[opcache]
opcache.enable=1
opcache.enable_cli=1

 JITを有効にする際には、opcache.jit オプションにJIT動作モードtracing (または function)を指定します。また、jit_buffer_sizeオプションにJITで用いるバッファの大きさ(以下の例では128Mバイト)を指定します。

opcache.jit=tracing
opcache.jit_buffer_size = 128M

OPcacheにおいてJIT機能は標準で無効になっています。PHP 8.4より前のバージョンでは、デフォルトでJIT機能を無効にするために、以下のようにバッファサイズを0にしていました。

opcache.jit=tracing
opcache.jit_buffer_size = 0

 この方法は、直観的ではなく、PHP 8.4では以下のようにデフォルトの設定が変更されました。

opcache.jit=disable
opcache.jit_buffer_size = 64M

PHP 8.4では、無効を表す新しい設定値disableが導入され、デフォルト値として指定されています。 バッファサイズが標準(64Mバイト)で良い場合、以下のように指定することで、JITを有効にすることができます。

opcache.jit=tracing

遅延オブジェクトの導入

 PHP 8.4では、長らく期待されていた遅延(Lazy)オブジェクトが導入されました。遅延オブジェクトは、プロパティのようなオブジェクトの状態の初期化をそれが参照または変更されるまで遅延する機能です。 この機能はSwiftやKotlinなどの他のスクリプト言語では実現されており、例えば、必要な時にデータを読み込むORM、必要な時に解析を行うJSONパーサで有用です。

早速、例を見てみましょう。まず、以下の簡単なクラスExampleを定義します。

class Example {
 public function __construct(
   public int $id, public string $name) {
     echo __METHOD__,"\n";
   }
}
$reflector = new ReflectionClass(Example::class);

 ここで、最後の行で、ReflectionClass のインスタンスとしてリフレクタを生成します。

 従来は、以下のようにnewによりクラスのインスタンスを生成して利用していました。この場合、インスタンスを作成する際に、自動的にクラスExampleのコンストラクタがコールされ、初期化が行われます。つまり、インスタンスの生成時に初期化が行われ、初期化は遅延していない状態となります。

$obj = new Example(1, "Taro");

この標準の動作に対して、遅延オブジェクトを利用すると、インスタンス生成の後、オブジェクトの内部状態の初期化を任意の間、遅延させることができます。

遅延オブジェクトの機能は、Reflectionクラスの追加機能として実装されています。遅延オブジェクトには、①Lazyゴースト、②Lazyプロキシの2種類があります。

Lazyゴーストの例を以下に示します。まず、オブジェクトの状態を初期化するイニシャライザを作成します。このイニシャライザの中では、コンストラクタ(__construct)をコールします。

$initializer = static function (Example $obj): void {
    $obj->__construct(1, "Taro");
};
$obj = $reflector->newLazyGhost($initializer);
$obj->id; // イニシャライザにより初期化:通常のオブジェクトに

 次にnewLazyGhostメソッドにイニシャライザを指定して、オブジェクトのインスタンスを生成します。このオブジェクトは、通常のオブジェクトと異なり遅延オブジェクトとなっており、プロパティのような内部状態が初期化されていない状態となります。最後の行でプロパティidにアクセスされており、このタイミングでイニシャライザが自動的にコールされて初期化が行われます。

Lazyゴーストを用いる、この初期化の後は、遅延オブジェクトは通常のオブジェクトになり、通常のオブジェクト区別することができなくなります。

 次に、Lazyプロキシの例を示します。この例においても、まず、イニシャライザを定義します。Lazyプロキシのイニシャライザにおいては、オブジェクトのインスタンスの実体を返します。

$initializer = static function (Example $obj): void {
    return new Example(1, "Taro");
};
$obj = $reflector->newLazyPyoxy($initializer);
$obj->id; // イニシャライザにより初期化:プロキシのまま

 この後、リフレクタのnewLazyProxyメソッドにイニシャライザを指定し、Lazyプロキシとしてオブジェクトのインスタンスを生成します。Lazyゴーストと同様に、この時点では、オブジェクトの状態は初期化されておらず、Lazyプロキシとして定義されています。

 最後の行で、プロパティidがアクセスされた時点で、イニシャライザが自動的にコールされ、オブジェクトの実体のインスタンスが生成されると共に、コンストラクタが実行され、状態の初期化が行われます。Lazyゴーストと異なるのは、このイニシャライザがコールされて初期化された後でも、PHPスクリプトで利用可能なオブジェクトはプロキシであり、必要に応じて実体からコピーしたデータにアクセスされるということです。このため、プロキシによりアクセスしたデータは、実体そのものではないことに注意が必要です。

 ここで、特定のプロパティにアクセスを行うことをトリガーを無効化して、遅延初期化を起動せずに変数を初期化することが可能です。以下のコードでは、setRawValueWithoutLazyInitializationメソッドにより、プロパティidについて、Lazy初期化を無効化した上で、初期化を行っています。

$reflector->getProperty('id')->setRawValueWithoutLazyInitialization(
$obj, 123);

この手順では、遅延初期化は起動しないため、nameなどの他のプロパティは初期化されないままとなっています。 Lazyゴーストのオブジェクトをvar_dump関数でダンプすると以下のように表示されます。

lazy ghost object(Example)#3 (1) {
  ["id"]=> int(123)
  ["name"]=> uninitialized(string)
}

 同様の動作をするコードを以下に示します。

$reflector->getProperty('id')->skipLazyInitialization($obj);
$reflector->getProperty('id')->setValue($obj, 123);

 skipLazyInitializationメソッドにより、プロパティ変数idの遅延初期化を無効にしています。この後、setValueメソッドでプロパティ変数idに初期値(123)を代入していますが、遅延初期化が無効化されているため、初期化は自動的には行われません。

 今回は、11月21日に正式リリースされたPHP 8.4の主要機能として、JITコンパイルの新方式、遅延オブジェクトの導入について解説しました。 次回以降もPHP 8.4のその他の変更点について紹介していきます。

関連記事

Webサイト運用の課題解決事例100選 プレゼント

Webサイト運用の課題を弊社プロダクトで解決したお客様にインタビュー取材を行い、100の事例を108ページに及ぶ事例集としてまとめました。

・100事例のWebサイト運用の課題と解決手法、解決後の直接、間接的効果がわかる

・情報通信、 IT、金融、メディア、官公庁、学校などの業種ごとに事例を確認できる

・特集では1社の事例を3ページに渡り背景からシステム構成まで詳解