【入門編】PHPのジェネレータ(Generator)とFiberのメモリ効率比較:遅延評価とリソース消費 – PHPコア・内部エンジンと高速化・並行処理の極意解析バイブル

こんにちは。日々の開発、本当にお疲れ様です。

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` するたびに値を返し、呼び出し側に制御を戻します(サスペンド)。

百聞は一見に如かず。実際に大量のデータを扱うコードをイメージしてみましょう。

  • 100万件のデータをメモリ効率よく遅延評価(Lazy Evaluation)で生成するジェネレータ
  • @return Generator
  • /
    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を用いた非同期風タスクのシミュレーション
  • /
    $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の内部エンジンは、私たちが思うよりもずっとエレガントに動いています。ぜひ、あなたの次のアーキテクチャ設計にこの知見を活かしてみてください。

    それでは、また次回の深淵でお会いしましょう。

    タイトルとURLをコピーしました