こんにちは。日々の開発、本当にお疲れ様です。
JavaやGo、Node.jsといった他のモダンな言語を深く経験されたあなたなら、PHPの「1リクエスト=1プロセス(またはスレッド)で完結し、レスポンス返却と共に全メモリが容赦なく解放される」という潔いアーキテクチャの美しさと、その裏腹にある限界を、肌で感じたことがあるのではないでしょうか。
「数百万件のレコードを処理したいのに、すぐに `Allowed memory size exhausted` で沈没する」
「非同期I/Oをやりたいだけなのに、コールバック地獄や重厚なライブラリに阻まれる」
そんな壁にぶつかった時、私たちはPHP 5.5で導入された「ジェネレータ(Generator)」と、PHP 8.1で革命をもたらした「Fiber(ファイバー)」という2つの強力な武器を思い出します。
どちらも「処理を一時停止し、後から再開する(サスペンド)」という点で同じように見えますが、PHPのZendエンジン内部(C言語レベルのメモリ空間)において、両者が消費するリソースやライフサイクルは全く異なります。
今回は、この2つの機構がPHPの裏側でどう動き、メモリをどう蝕み、あるいは救っているのかを、Zend VMの息吹を感じながら紐解いていきましょう。ここを理解すると、PHPのメモリ効率のコントロールが見違えるほど綺麗に見えるようになりますよ。
—
1. Zend VMにおける「実行コンテキスト」の正体
まず、PHPのコードが実行されるとき、裏側で何が起きているかを思い出してください。
PHPのスクリプトは、Zendコンパイラによってオペコード(Opcode)に変換され、Zend VM上で実行されます。
通常の関数呼び出しであれば、コールスタック(Call Stack)上に「スタックフレーム(`zend_execute_data`)」が積み上がり、関数がリターンすればそのフレームは即座に破棄されます。
しかし、ジェネレータやFiberは、この「実行途中のスタックフレーム」をヒープメモリ上に退避させ、一時停止するという共通の特技を持っています。ここからがそれぞれの真骨頂です。
—
2. ジェネレータ(Generator):極限まで削ぎ落とされた遅延評価の美学
まずは、古くからあるジェネレータを見てみましょう。
大規模なCSVやデータベースの全件フェッチをメモリ効率よく処理したい時、あなたも `yield` を使ったことがあるはずです。
ジェネレータのメモリ構造
ジェネレータの実体は、PHPの内部構造体では `zend_generator` として表現されます。特筆すべきは、Fiberのような独自のコールスタックを持たない点です。
ジェネレータは、元の関数のオペコード配列へのポインタと、ローカル変数を保持する最小限の `zend_execute_data`、そして現在の実行位置(インディケータ)だけをヒープ上に保持します。関数が `yield` するたびに値を返し、呼び出し側に制御を戻します(サスペンド)。
百聞は一見に如かず。実際に大量のデータを扱うコードをイメージしてみましょう。
/
function yieldHugeData(): Generator {
for ($i = 1; $i <= 1000000; $i++) {
// ここで1行分の文字列を生成するが、メモリには「この瞬間の1行分」しか存在しない
// 配列(array)として一気にメモリに展開すると数十MB〜数百MBを消費するが、
// ジェネレータであれば数KBのオーバーヘッドで済む。
yield $i => “User_Data_ID_” . $i;
}
}
// 実行時のメモリ使用量を計測する
$memoryBefore = memory_get_usage(true);
$generator = yieldHugeData();
// まだこの時点ではループは走っておらず、Zend VM上では単にジェネレータオブジェクトがインスタンス化されただけ
foreach ($generator as $id => $data) {
// 最初の10件だけ処理して抜けるようなケースでも、100万件分のメモリは一切消費されない
if ($id > 10) {
break;
}
}
$memoryAfter = memory_get_usage(true);
echo “消費メモリ増加分: ” . ($memoryAfter – $memoryBefore) . ” バイト\n”;
なぜジェネレータのメモリ効率は圧倒的なのか?
ジェネレータの強みは、「状態の保持コストが、通常の関数呼び出しのスタックフレーム1つ分+αしかない」という点にあります。
配列であれば、要素数に比例して `Bucket` 構造体のハッシュテーブルやZvalのオーバーヘッドが増大しますが、ジェネレータは「次に何をすべきか」のイテレーション状態しか持ちません。
「データを一括で持たず、必要な瞬間に計算して流し込む(遅延評価)」というアプローチにおいて、ジェネレータはPHPのコアにおいて最も軽量かつ堅牢な仕組みです。
—
3. Fiber(ファイバー):協調的マルチタスクの代償と可能性
では次に、PHP 8.1のスターである「Fiber」を見てみましょう。
Fiberは、いわゆる「緑色のスレッド(Green Threads)」や「ユーザー空間の軽量スレッド」であり、コールスタックの深さに関わらず、コードの任意の場所で処理を中断し、別の処理に切り替える(コンテキストスイッチ)ことができます。
Fiberのメモリ構造:何が重いのか?
Fiberの内部構造(`zend_fiber_context`)は、ジェネレータに比べて圧倒的にリッチ(=重い)です。
ジェネレータがあくまで「1つの関数の中断」に特化していたのに対し、Fiberは独自のC言語レベルのコールスタック(通常はデフォルトで数MBの仮想メモリ領域が割り当てられます)を抱え込みます。
これにより、次のような芸が可能になります。
- 深くネストした関数呼び出しの最深部から、一気にトップレベルへサスペンドする。
- 非同期I/Oの完了を待つ間、別のタスク(HTTPリクエストの送信など)に処理を明け渡す。
しかし、これは裏を返すと「ジェネレータよりも遥かに多くのメモリと初期化コストを消費する」ことを意味します。
/
$fiber = new Fiber(function (): void {
echo “Fiber内部: 処理を開始します\n”;
// 深いコールスタックの途中でもサスペンド可能
$value = Fiber::suspend(‘一時停止からのデータ’);
echo “Fiber内部: 再開しました。受け取った値 -> {$value}\n”;
});
// Fiberを開始(ここでFiber専用のスタックが確保される)
$valueFromFiber = $fiber->start();
echo “メイン側: Fiberから受け取った値: ‘{$valueFromFiber}’\n”;
// Fiberに値を渡して再開
$fiber->resume(‘メインからの贈り物’);
ここで重要なのは、「Fiberは単なる遅延評価の道具ではない」ということです。
Fiberの本質は「実行コンテキスト(コールスタック全体)の完全な制御権の移譲」であり、メモリ消費の観点から見れば、ジェネレータよりもはるかにヘビーウェイトな存在なのです。
—
4. ジェネレータ vs Fiber:メモリ効率とユースケースの徹底比較
ここまでの話を整理してみましょう。実際のアーキテクチャ設計において、どちらを選択すべきかの判断基準は非常に明確です。
| 比較項目 | ジェネレータ (Generator) | ファイバー (Fiber) |
| :— | :— | :— |
| 主な目的 | メモリ効率の良い「データのストリーミング / 遅延評価」 | 処理の「協調的マルチタスク(非同期処理・中断/再開)」 |
| メモリ消費 | 極小 (通常の関数フレーム程度) | 中〜大 (独立したコールスタック領域を保持) |
| スタック管理 | なし(単一のイテレーション状態のみ) | あり(独立したスタックコンテキスト) |
| 適した場面 | 大規模CSV読み込み、DBのカーソルフェッチ、無限ストリーム | 非同期HTTPクライアント、イベントループの構築、複雑なステートマシン |
アーキテクトとしての判断指針
もしあなたの目的が単に「大量のメモリを節約しながらデータを1件ずつ処理したい」だけであるならば、絶対にFiberを使ってはいけません。 それは牛刀で鶏を割くようなものであり、不要なスタックメモリの確保によってFPMプールのメモリプレッシャーを高める結果になります。その場合は、迷わずジェネレータを選択してください。
逆に、AMP (Amp) や Revolt などのモダンな非同期PHPエコシステムのように、「単一のプロセス内で複数のI/O待ちタスクを並行して回したい」という非同期Webアプリケーションの基盤を作るのであれば、Fiberこそが唯一無二の解となります。
—
5. まとめ:PHPの裏側を視る眼を持つということ
PHPは、長年にわたり「リクエストライフサイクルが短いシンプルさ」を武器に進化してきました。しかし、モダンなWebアプリケーションに求められるスループットやリアルタイム性は、もはや単なる「書いて動くコード」だけでは太刀打ちできない領域に達しています。
- ジェネレータは、Zend VMのスタック機構を巧みにハックし、巨大なデータセットを「今、必要な分だけ」メモリに顕現させるための、洗練されたカミソリのようなツールです。
- Fiberは、PHPに本格的な非同期パラダイムをもたらし、コールスタックそのものを手元で自在に操るための、重厚かつ強力なエンジンです。
この2つの違いをメモリレイアウトの深部から理解していれば、コードレビューやパフォーマンスチューニングの際に「なぜこの設計ではメモリリークするのか」「なぜここでFiberを使うべきではないのか」を、自信を持ってチームに説明できるようになります。
PHPの内部エンジンは、私たちが思うよりもずっとエレガントに動いています。ぜひ、あなたの次のアーキテクチャ設計にこの知見を活かしてみてください。
それでは、また次回の深淵でお会いしましょう。