こんにちは。日々、巨大なトラフィックと向き合い、コードのパフォーマンスを極限まで絞り出そうと格闘されていることと思います。JavaやGo、Node.jsといった他言語の高水準な並行処理モデルを知るエンジニアほど、PHPの進化、特にモダンなPHP 8世代の内部挙動に触れたとき、「おや、これほど洗練されていたのか」と驚かされることが多いはずです。
今日は、大規模データ処理や非同期的なワークフローを設計する上で避けて通れない、「ジェネレータ(Generator)」と「Fiber(ファイバー)」のメモリ効率と内部アーキテクチャの違いについて、PHPのエンジン(Zend VM)の裏側を覗きながら紐解いていきましょう。
ここを理解すると、PHPの実行モデルがグッと立体的に見えてきますよ。
—
1. そもそも、なぜPHPで「メモリ効率」が問題になるのか?
他の言語から来た開発者が最初に驚くのは、PHPの「1リクエスト=1プロセス(またはスレッド)完結」というシェアード・ナッシング(Shared-Nothing)アーキテクチャの潔さです。リクエストが終われば、Zendエンジンが抱えていたメモリはOSに一括返還されます。
しかし、だからといって「1リクエスト内で数百万件のレコードをメモリに展開してよい」ということにはなりません。
例えば、数百万行のCSVや巨大なJSONをデータベースから一括フェッチしようとすると、お馴染みの `Allowed memory size exhausted` が容赦なく襲いかかります。Zend VMの変数管理コンテナである `zval` や、配列の内部表現である `HashTable` は非常にリッチに作られているため、数万件のオブジェクトや連想配列を素朴にメモリ上に並べた瞬間、数ギガバイトのRAMが簡単に蒸発してしまうのです。
この「巨大なデータの壁」をスマートに乗り越えるために用意されたのが、ジェネレータであり、さらに複雑な協調的マルチタスキングを実現するFiberです。
—
2. ジェネレータ(Generator):遅延評価の美学とZend VMのスタックフレーム
まずは、PHP 5.5で導入されて以来、地味ながら極めて強力な武器であるジェネレータを見てみましょう。
/
function readHugeCsv(string $filePath): Generator {
$handle = fopen($filePath, ‘r’);
if ($handle === false) {
throw new RuntimeException(“ファイルをオープンできませんでした。”);
}
try {
while (($row = fgetcsv($handle)) !== false) {
// yieldによって、値を1つずつ呼び出し元に「返す」のではなく「渡す」
yield $row;
}
} finally {
// イテレーションが途中で破棄されても確実にリソースを解放
fclose($handle);
}
}
// 実行時のメモリ使用量は、ファイルサイズにかかわらず常にO(1)に収まります
foreach (readHugeCsv(‘/path/to/huge_dataset.csv’) as $row) {
// 1行ずつ安全に処理
// echo $row[0];
}
内部で何が起きているのか?(Zend VMの視点)
通常の関数は、呼び出されるとZend VM上で新しいスタックフレーム(`zend_execute_data`)が作られ、ローカル変数や実行コンテキストが展開され、return時にすべて消滅します。
一方、`yield` を含む関数は、PHPのコンパイル時に `Generator` クラスのインスタンスを返す特殊な関数 に変換されます。
`foreach` や `Iterator` メソッドを通じてジェネレータが駆動されると、以下のようなサイクルが回ります。
1. 実行のサスペンド(一時停止): `yield` に達すると、Zend VMはその時点でのローカル変数や実行位置(opcodeポインタ)の状態を、ヒープ上に確保された `Generator` オブジェクトに退避させます。
2. 制御の返還: 呼び出し元(`foreach`側)に値が渡されます。
3. レジューム(再開): 次のループで値が要求されると、退避されていたコンテキストが復元され、`yield` の次の行から実行が再開されます。
ここがポイントです:
ジェネレータは「データをオンデマンドで生成する(Lazy Evaluation)」ため、メモリ上には常に「現在の1行(あるいは極めて小さなバッファ)」しか存在しません。数百万行のデータを扱おうとも、メモリ消費量は常に一定(O(1))です。
—
3. Fiber(ファイバー):スタックを持つ軽量スレッドの正体
では、PHP 8.1で導入された Fiber は何が違うのでしょうか。
Fiberは、いわゆる「協調的マルチタスキング(Cooperative Multitasking)」を実現するためのプリミティブであり、任意の場所で実行を中断し、別のコンテキストに処理を移すことができます。
/
$fiber = new Fiber(function (string $name): string {
echo “Fiber開始: {$name}\n”;
// 処理の途中で自発的にサスペンドし、親へ値を渡す
$received = Fiber::suspend(“データをくれと言われました”);
echo “Fiber再開: 受け取った値は ‘{$received}’ です\n”;
return “処理完了”;
});
// Fiberを起動
$valueFromFiber = $fiber->start(“テストタスク”);
echo “メイン側で受取: {$valueFromFiber}\n”;
// Fiberに値を渡して再開させる
$finalResult = $fiber->resume(“特製みかんジュース”);
echo “Fiberからの最終応答: {$finalResult}\n”;
ジェネレータとFiberの決定的な違い
「おや、Fiberも途中で一時停止して値をやり取りするなら、ジェネレータと同じではないか?」と思われたかもしれません。しかし、アーキテクチャの観点から見ると、両者は似て非なるものです。
| 特性 | ジェネレータ (Generator) | ファイバー (Fiber) |
| :— | :— | :— |
| 主な用途 | データのイテレーション(ストリーミング) | 処理の流れの制御、非同期I/Oの多重化 |
| コールスタック | フラット(単一の関数コンテキストの保持) | ディープ(ネストされた関数呼び出しのスタック全体を保持) |
| 双方向の制御 | ほぼ単方向(`yield` で返し、`send()` で一応戻せるが限定的) | 完全な双方向(どこからでも `suspend` と `resume` が可能) |
| メモリコスト | 極めて軽量(`Generator` オブジェクト1つ分) | 中程度(独自のコールスタック領域を持つため、ジェネレータより重い) |
ジェネレータは、あくまで「ひとつの関数の内部状態」を維持するものです。もしジェネレータの内部で別の関数Aを呼び出し、その関数Aのさらに内部から値を `yield` したい場合、PHP 7以前では非常に面倒でした(PHP 7の `yield from` で幾分マシにはなりましたが)。
一方、Fiberは「独自のコールスタック」を丸ごとヒープ上に保持します。
つまり、Fiberの中で関数Aを呼び、その中の関数Bを呼び、さらにその中の関数Cの深い階層からでも、一発で外側のメインループへ処理を巻き戻す(サスペンドする)ことができるのです。
—
4. 大規模データ処理における「正しい選択基準」
では、実際のWebアプリケーションやバッチ処理において、私たちはどちらを選択すべきなのでしょうか?
アーキテクトとしての判断基準は非常にシンプルです。
パターンA:ただデータを順次処理したいだけなら「ジェネレータ一択」
数百万件のDBレコード、巨大なログファイル、CSVのパースなど、「データを1件ずつ流し込んで破棄する」というパイプライン処理であれば、ジェネレータを使用してください。
余計なコールスタックを持たないためメモリ効率が最高であり、Zend VMのオーバーヘッドも最小限に抑えられます。
パターンB:複数の非同期タスクをインターリーブ(交互に実行)させたいなら「Fiber」
例えば、複数の外部API(HTTPクライアント)へ並行してリクエストを送り、レスポンスを待つ間に別の処理を進めたい場合や、ゲームサーバーのようなイベント駆動型のループをPHPで書きたい場合です。
Amp (v3) や Revolt といったモダンなPHP非同期エコシステムは、この Fiber を基盤として構築されており、コールバック地獄(Promises)に頼ることなく、同期的で美しいコードで非同期I/Oを実現しています。
ただし、注意してください。Fiberは「メモリを節約するための仕組み」ではありません。 むしろ、スタックを保持する分、ジェネレータよりもメモリを消費します。Fiberの真価は、メモリ効率ではなく「制御フローの自由度と非同期性の美しさ」にあります。
—
5. アーキテクトからのメッセージ
PHPは「スクリプト言語だから遅い、メモリを食う」という古いステレオタイプは、もはや過去のものです。Zend JITの導入、厳格な型システム、そして今回解説したジェネレータやFiberといったモダンなプリミティブを正しく理解し、適材適所で使い分けることで、PHPはNode.jsやGoに匹敵する堅牢でハイパフォーマンスなバックエンドエンジンへと変貌を遂げます。
「なぜこの書き方がメモリに優しいのか」
「Zend VMはこのコードをどう解釈しているのか」
その一歩深い視点を持つだけで、あなたの書くPHPコードは、美しく、スケールする、真にプロフェッショナルなシステムへと進化するはずです。
次回の開発の際には、ぜひこのメモリの裏側の景色を思い出してみてください。きっと、コードの切れ味が変わるのを実感できるはずです。