PHPはHTTPリクエストが来るたびに全てのPHPコードをバイトコードに変換し、そして実行しています。
毎回そんなことやってるのにあれだけ速度が出るのは驚異的ですが、それでもやはりコンパイルにかかる時間だけどうしても遅くなってしまいます。
そこで、もっと高速化するためにOPcacheのような仕組みが存在します。
これはバイトコードをメモリ上に保持し、リクエストを超えて使い回すことでコンパイルの手間を省略し、高速化を実現するというものです。
効果はというと、単純なものでもターンアラウンドタイムが2/3、大きなフレームワークでは半分以下と、お手軽かつ強力な効果があります。
とはいえOPcacheには、元のPHPファイルに変更があるかどうかを監視したりといった僅かなコストが残っています。
特にバイトコードはファイル単位でしかキャッシュできないらしく、extendsなどで別のファイルを参照しているときは毎回再リンクしているようです。
そこで新たにPreloadingというRFCが提出されました。
提出というか既に投票フェーズに入っていて、投票期間は2018/11/14までですが、賛成46/反対0でほぼ導入確定です。
RFC: Preloading
書式
php.ini
に新しいディレクティブ、opcache.preload
が追加されます。
これは単一のPHPファイルを指定し、Webサーバの起動時にこのファイルが読み込まれます。
その中でopcache_compile_file()を指定すると、そのファイルがキャッシュされます。
opcache.preload="path/to/preload.php"
opcache_compile_file('path/to/hoge.php');
opcache_compile_file('path/to/fuga.php');
これでサーバの再起動時にhoge.php
とfuga.php
がキャッシュされます。
以後このサーバでは、その中で定義されているクラスや関数を、ネイティブ関数と同じようにいつでもどこでも自由に使うことができるようになります。
RFCにはZend Frameworkをまるごとプリロードする例が載っていました。
function _preload($preload, string $pattern = "/\.php$/", array $ignore = []) {
if (is_array($preload)) {
foreach ($preload as $path) {
_preload($path, $pattern, $ignore);
}
} else if (is_string($preload)) {
$path = $preload;
if (!in_array($path, $ignore)) {
if (is_dir($path)) {
if ($dh = opendir($path)) {
while (($file = readdir($dh)) !== false) {
if ($file !== "." && $file !== "..") {
_preload($path . "/" . $file, $pattern, $ignore);
}
}
closedir($dh);
}
} else if (is_file($path) && preg_match($pattern, $path)) {
if (!opcache_compile_file($path)) {
trigger_error("Preloading Failed", E_USER_ERROR);
}
}
}
}
}
set_include_path(get_include_path() . PATH_SEPARATOR . realpath("/var/www/ZendFramework/library"));
_preload(["/var/www/ZendFramework/library"]);
注意事項
この方法で作成したキャッシュは、サーバが生きているかぎり永続します。
元のPHPファイルを変更してもキャッシュは更新されないし、たとえopcache_reset()を使ったとしても消えません。
更新する方法はサーバの再起動だけとなります。
つまり、OPcacheの通常の使用法では残っていた元ファイル監視などのコストを排除し、さらなる高速化を図ったものといえます。
制限
プリロードの恩恵を受けられるのは、親クラス・インターフェイス・トレイト・定数の定義をプリロードの範囲内で解決できるクラスだけです。
それが満たされないクラスは普通のOpcacheと同じようにopcache SHMに格納されます。
ってなってるんだけどSHMって何だ?
/dev/shm
のこと?
下位互換性のない変更点
明示的に使用しないかぎり何も起らないので、何もしなければ影響はありません。
プリロードされたクラスや関数はfunction_exists()
やclass_exists()
に常にtrueを返します。
2種類以上のアプリケーションを走らせているサーバでは、うっかりPreloadingを使うと問題が起こる可能性があります。
2つのアプリでそれぞれ独自のh()
関数を作っていたとして、片方をPreloadingに載せると、もう片方がCannot redeclare
のFatal errorになります。
導入されるバージョン
PHP 7.4。
パフォーマンス
プリロード1000ファイル、通常ロード150個のテストで、ターンアラウンドタイムが36.4ミリ秒から29.1ミリ秒になりました。
感想
opcache.preload
で読み込んだ関数やクラスは、strlen()
やException
といったビルトイン関数・クラスと同じように前準備なしで動くようになります。
つまり、これまではわざわざエクステンションを作らなくてはいけなかったものが、素のPHPでできるようになるということです。
そのかわり、それらは普通のPHPファイルのようにソースを変更しても変更が反映されることはなくなり、反映にはサーバの再起動が必要となります。
この点もエクステンションっぽいですね。
これが普及すれば、フレームワークの本体側はpreloadに載せておいて、ユーザによる変更部分のみ通常のPHPとして読み込む、といった運用が増えていくことでしょう。
バージョンアップごとにApache再起動させられるのは手間ですが。