Zend Engineの仕組み
PHPがソースコードからオペコードを経て実行されるまでを追います。
「Zend Engineの仕組み」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。
エンジンの内部
PHPはソースコードを1行ずつ解釈実行するわけではありません。Zend Engineはスクリプトをオペコードと呼ばれる中間表現にコンパイルし、その後、仮想マシンがそれらを実行します。このパイプラインを理解すると、パフォーマンス、OPcache、JITの仕組みが分かります。
このレッスンでは、ソースコード → トークン → AST → オペコード → VMによる実行の流れを学びます。
パイプライン
PHPファイルへの各リクエストは、次の4つのフェーズを経ます。
- 字句解析 — ソーステキスト → トークン(re2cベースのスキャナ)
- 構文解析 — トークン → 抽象構文木(Bison文法)
- コンパイル — AST → オペコード配列(op_array)
- 実行 — Zend VMがオペコードを走査します
OPcacheがない場合、最初の3つのフェーズはすべてのリクエストで繰り返されます。
トークン化
字句解析器は文字をT_VARIABLE、T_ECHO、T_STRINGのようなトークンに変換します。PHPではtoken_get_all() / PhpTokenを通じてこの段階を利用でき、PHP-CS-Fixerのようなツールもまさにこれを使用しています。
<?php
$src = '<?php $x = 1 + 2; echo $x;';
foreach (PhpToken::tokenize($src) as $tok) {
if ($tok->isIgnorable()) continue; // skip whitespace
printf("%-12s %s\n", $tok->getTokenName(), trim($tok->text));
}
?>AST
トークンはノードのツリーに構文解析されます。たとえば、子ノードとして変数と二項演算式を持つ代入ノードになります。PHPは内部でこのASTを構築し、nikic/php-parserはPHPStanのような静的解析ツール向けに、ユーザー空間で同等のASTを再構築します。
<?php
// Conceptual AST for: $x = 1 + 2;
//
// AST_ASSIGN
// ├── AST_VAR ($x)
// └── AST_BINARY_OP (+)
// ├── 1
// └── 2
//
// At compile time PHP folds 1 + 2 into a literal 3
// (constant folding) before generating opcodes.
echo "AST drives opcode generation\n";
?>オペコード
コンパイラはop_arrayというオペコードのフラットなリストを生成します。各オペコードにはオペコード番号(例:ZEND_ADD、ZEND_ECHO、ZEND_ASSIGN)があり、最大2つのオペランドと1つの結果を持ちます。それぞれは、コンパイル済み変数(CV)、一時値(TMP)、または定数です。
<?php
// Opcodes for: $x = 1 + 2; echo $x;
//
// line op operands result
// --- ------------ ----------------- -------
// 1 ADD 1, 2 ~0
// 1 ASSIGN $x, ~0
// 1 ECHO $x
// 1 RETURN 1
//
// ~0 is a temporary; $x is a compiled variable (CV).
echo "op_array is what OPcache stores\n";
?>オペコードの確認
VLD拡張機能またはOPcacheのopcache.opt_debug_levelを使うと、生成されたオペコードをダンプできます。これにより、定数畳み込み、デッドコード削除、制御フローがどのようにJMP/JMPZオペコードになるかを確認できます。
# Dump opcodes with VLD
php -d vld.active=1 -d vld.execute=0 script.php
# Or via OPcache optimizer debug (pre/post optimization)
php -d opcache.opt_debug_level=0x10000 script.php # before opt
php -d opcache.opt_debug_level=0x20000 script.php # after optコンパイル済み変数(CV)
ローカル変数は、コンパイル済みコードではハッシュテーブル検索ではありません。コンパイラがそれぞれに番号付きのCVスロットを割り当てます。$xに2回目にアクセスするときは、シンボルテーブルの検索ではなく配列インデックスによるアクセスになります。これがローカル変数が高速な大きな理由の1つです。
<?php
// Each named local gets a fixed CV slot at compile time:
// $a -> CV0 $b -> CV1 $sum -> CV2
function add(int $a, int $b): int {
$sum = $a + $b; // ADD CV0, CV1 -> CV2
return $sum; // RETURN CV2
}
echo add(2, 3) . PHP_EOL;
?>VMの実行ループ
エグゼキュータ(execute_ex)はop_arrayを走査します。各オペコードはハンドラ関数に対応しており、PHPではこのディスパッチを巨大なswitch、computed goto、またはハイブリッドとして構築できます(対応するコンパイラでは、ハイブリッドがデフォルトで最速です)。oplineポインタは進み、JMPオペコードによって制御フローに応じて移動します。
<?php
// if ($n > 0) echo 'pos'; compiles roughly to:
//
// IS_SMALLER 0, $n -> ~T
// JMPZ ~T, ->L1 ; if false, skip
// ECHO 'pos'
// L1:
// RETURN 1
//
// The VM follows opline; JMPZ rewrites it conditionally.
$n = 5;
if ($n > 0) echo "pos\n";
?>OPcacheの位置付け
OPcacheは共有メモリ内のコンパイル済みop_arrayをキャッシュし、後続のリクエストでは字句解析・構文解析・コンパイルを省略します。また、オプティマイザのパス(定数畳み込み、デッドコード削除、オペコード融合)も実行します。VMはリクエストごとにキャッシュされたオペコードを引き続き実行します。ここで後からJITが効果を発揮します。
関数呼び出しのオーバーヘッド
関数呼び出しでは新しいVMスタックフレームが積まれます。オペコードは、INIT_FCALL、引数ごとのSEND_VAL/SEND_VAR、続いてDO_FCALLという順序になります。この仕組みを知ると、小さな関数を過剰に呼び出すと測定可能なコストが発生する理由や、ホットループでインライン化/JITが重要になる理由が分かります。
<?php
// square($x) compiles to a call sequence:
// INIT_FCALL 'square'
// SEND_VAR $x
// DO_FCALL -> ~R
// ASSIGN $y, ~R
function square(int $x): int { return $x * $x; }
$total = 0;
for ($i = 1; $i <= 5; $i++) {
$total += square($i); // one call sequence per iteration
}
echo $total . PHP_EOL; // 1+4+9+16+25 = 55
?>リクエストの終了
実行後、PHPはリクエストを解体します。実行中の変数を解放し、出力をフラッシュしてから、エンジンのアロケータアリーナをリセットします。shared-nothing FPMモデルでは、各リクエストがクリーンな状態から始まります。そのため、あるリクエストの致命的エラーが別のリクエストを破壊することはありません。OPcacheの共有op_arraysはリクエスト間で存続しますが、エグゼキュータの状態は存続しません。
確認問題
再コンパイルを省略するために、OPcacheは具体的に何を保存しますか?
まとめ
Zendのパイプラインは、字句解析 → 構文解析 → コンパイル → 実行です。ソースコードはトークンになり、次にAST、そしてVMの実行ループが処理するオペコードのop_arrayになります。ローカル変数は番号付きのCVスロットに保存され、関数呼び出しではスタックフレームが積まれます。OPcacheはop_array(およびオプティマイザのパス)をキャッシュするため、リクエストごとに繰り返されるのは実行だけです。
よくある質問
「Zend Engineの仕組み」レッスンは無料ですか?
はい。「Zend Engineの仕組み」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。
「Zend Engineの仕組み」で何を学びますか?
PHPがソースコードからオペコードを経て実行されるまでを追います。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
PHP Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。
「Zend Engineの仕組み」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このPHP Academyレッスンでコードを書いて実行できますか?
はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Zend Engineの仕組み
- メモリ管理とガベージコレクション
- OPcacheとJITコンパイル
- Cで基本的なPHP拡張機能を書く