こんにちは。普段はフレームワークのルーティングやORMの便利さに囲まれて開発をしていると、「PHPって、リクエストが来たらどうやって動いているんだろう?」と、ふと裏側の仕組みが気になりますよね。
JavaやGo、Node.jsといった常駐型のランタイムと違い、PHPは「リクエストごとにプロセスが立ち上がり、スクリプトを解釈し、レスポンスを返したらすべてを綺麗にリセットして消滅する」という、極めてユニークで潔いライフサイクルを持っています(FPM環境の場合)。
このライフサイクルの中で、PHPの心臓部である Zend VM と、その実行コンテキストである `Executor Globals`(通称 `EG`) が裏側でどのようなダンスを踊っているのか、少し覗いてみたことはありませんか?
ここを理解すると、「なぜこの書き方はメモリに優しいのか」「なぜリクエストをまたぐグローバル変数が危険なのか」「OPcacheは何をキャッシュしているのか」という疑問の霧が晴れ、PHPという言語の美しさと合理性に感動できるようになりますよ。
それでは、Zend VMの深淵へ、一緒に潜っていきましょう。
—
1. Zend VMと `Executor Globals (EG)` の正体
私たちが書いたPHPのコードは、LexerとParserによって抽象構文木(AST)に変換され、最終的にZend VMが解釈できる「オペコード(Opcode)」へとコンパイルされます。
Zend VMは、いわば「PHP専用の仮想的なCPU」です。この仮想CPUがコードを実行する際、現在の実行状態(コールスタック、定義されている関数やクラス、シンボルテーブル、スーパーグローバル変数など)のすべてを保持している巨大な構造体が `Executor Globals`、マクロ名で言う `EG` です。
C言語のソースコードレベルで見ると、`EG` は `zend_executor_globals` という巨大な構造体として定義されています。
/ 実際のPHPソースコード(Zend/zend_globals.h)の概念に近いイメージ /
struct _zend_executor_globals {
zval uninitialized_zval;
zval error_zval;
/ シンボルテーブル(ローカル・グローバル変数空間) /
zend_symtable symbol_table;
HashTable function_table;
HashTable class_table;
HashTable constants_table;
/ コールスタックのポインタ /
zend_execute_data current_execute_data;
/ 例外ハンドリングやモジュール固有の状態… /
};
リクエストライフサイクルとメモリの現実
Webサーバー(Nginx + PHP-FPM)環境において、PHP-FPMのワーカープロセスは長寿命です。しかし、1つのリクエストが始まるとき、`EG` の中身は初期化され、リクエストが終わると破棄(あるいは再初期化)されます。
ここで重要なのは、OSのプロセス空間内において、この `EG` がどのように扱われているかです。
C言語のグローバル変数として定義されているため、プロセスが生存している限りメモリ上に存在しますが、リクエストの境界ごとに `request_startup()` と `request_shutdown()` が走り、シンボルテーブルやメモリプール(emalloc)が綺麗に巻き戻されます。
これが、「PHPではリクエスト間でデータが汚染されない(共有されない)」という鉄の安全神話の正体です。
—
2. オペコード実行とコンテキストスイッチのオーバーヘッド
Zend VMがオペコードを1つずつ実行していくループ(通称:Zend Execute Engine)は、C言語で書かれた巨大な `switch` 文、あるいは高速化のために近年のPHP(PHP 8以降)で導入された「Computed Goto(間接ジャンプ)」によって駆動されています。
このループの中で、VMは次のようなサイクルを毎秒何百万回も回しています。
1. `current_execute_data`(現在の実行コンテキスト)から次のオペコードを取得する。
2. オペコードが指定するオペランド(引数や変数)を `EG` のシンボルテーブルから解決する。
3. 演算を行い、結果を別の zval(PHPの変数コンテナ)に格納する。
4. ポインタを進める。
ここで意識しておきたいのが、「コンテキストスイッチとメモリ参照のコスト」です。
例えば、関数を呼び出すたびに、Zend VMは新しい `zend_execute_data` をスタックに積み、`EG(current_execute_data)` を書き換えます。これがPHPの文脈における「関数呼び出しのオーバーヘッド」です。
他のコンパイル言語(GoやC++など)のインライン展開やレジスタベースの呼び出しと比べると、PHPはスタックベースの仮想マシンであり、かつ変数実体が `zval` という構造体(型情報や参照カウンタを持つ)であるため、どうしても命令実行あたりのCPUサイクルが多くなります。
実際にコードの裏側を脳内トレースしてみる
例えば、次のような非常にシンプルなPHPコードがあったとします。
`ASSIGN` オペコード: `$a` という名前のキーと整数 `10` を持つ `zval` が、現在のスコープのシンボルテーブル(`EG(symbol_table)`)に登録されます。
2. `ASSIGN` オペコード: 同様に `$b` が登録されます。
3. `ADD` オペコード: `$a` と `$b` の値をフェッチし、加算を行い、その結果を新しい `zval` として `$c` に紐付けます。
もしこのコードがループ内で10万回実行されたとしたら、Zend VMは10万回もシンボルテーブルのハッシュルックアップ(キー名から変数を引く処理)を行うことになります。
—
3. OPcacheとJITが変えたゲームのルール
「毎回シンボルテーブルから変数を引くなんて、遅いじゃないか」
鋭いあなたならそう気づくはずです。そこで登場するのが OPcache です。
OPcacheは、ソースコードをパースしてオペコードに変換する「重い処理」を最初に1回だけ行い、その結果(shared memory)を共有メモリにキャッシュします。これにより、リクエストごとのコンパイルコストがほぼゼロになります。
さらに、PHP 8で導入された JIT(Just-In-Time)コンパイラ は、このZend VMのオペコード実行ループそのものをバイナリ(機械語)に翻訳します。
通常、Zend VMは「オペコードを読み、Cのコードに分岐し、変数をフェッチする」というインタプリタの仲介を挟みますが、JITが有効になると、頻繁に実行されるホットスポット(ループや数値計算など)が、CPUが直接理解できるネイティブ命令に置き換わります。
これにより、CPUのレジスタを直接活用できるようになり、`EG` を介したハッシュテーブルのルックアップというオーバーヘッドをバイパスできるようになるのです。
—
4. アーキテクトとして知っておくべき実務への応用
ここまで、Zend VMと `Executor Globals` の深層を見てきました。この知識は、日々の開発やトラブルシューティングでどのように活きるでしょうか?
① 「グローバル変数や `$_SESSION` の多用」がなぜ重くなるのか
`EG` 内のシンボルテーブルは、本質的にはハッシュテーブル(PHPの内部配列と同じ構造)です。グローバルな空間やリクエスト全体で共有されるコンテキストにデータを詰め込みすぎると、変数を参照するたびにハッシュの衝突解決やルックアップのコストが発生します。
クリーンなスコープ(関数やメソッドのローカル変数)を意識したコードを書くことは、Zend VMのレジスタ(に近いスタック上の位置)へのアクセスを効率化し、キャッシュヒット率を高めることに直結します。
② フレームワークのオートローダーとシンボルテーブル
モダンなフレームワーク(LaravelやSymfonyなど)は、数千ものクラスやファイルを扱います。これらは初回のクラスロード時に `EG(class_table)` に登録されます。
OPcacheが正しく有効化され、ファイルシステムへのI/Oがキャッシュされている環境であれば、この `class_table` への登録コストはリクエストをまたいで共有されます(正確にはOPcacheのshared memoryからプロセスごとにマッピングされます)。
本番環境で OPcache のメモリ割り立て(`opcache.memory_consumption`)が不足すると、キャッシュアウトが発生し、毎リクエストごとにパースとテーブル構築が走るという悲劇が起きます。ここを監視することが、Webアーキテクトとしての最初の仕事になります。
—
おわりに
PHPは、「動的で、シンプルで、誰でも簡単に書ける言語」としてスタートしました。しかし、その内部で動いているZend VMと `Executor Globals` の世界は、C言語の洗練されたメモリ管理と、極限まで最適化された仮想マシンのロジックで満ち溢れています。
「リクエストごとに世界が生まれ、終わったら綺麗に消え去る」
この圧倒的なシンプルさと安全性の裏側で、Zend VMは今日も猛烈な速度でオペコードを解釈し続けています。コードを書くときに「あ、今VMはシンボルテーブルからこの変数を引いているんだな」「ここはOPcacheが綺麗に最適化してくれるはずだな」と、その裏側の息吹を感じられるようになれば、あなたの書くPHPコードは、もう一段、美しく、強靭なものに変わるはずです。
それでは、また次の深淵でお会いしましょう。