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

こんにちは。日々、巨大なトラフィックと向き合い、コードのパフォーマンスを極限まで絞り出そうと格闘されていることと思います。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を用いた非同期的なタスク制御のイメージ
  • /
    $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コードは、美しく、スケールする、真にプロフェッショナルなシステムへと進化するはずです。

    次回の開発の際には、ぜひこのメモリの裏側の景色を思い出してみてください。きっと、コードの切れ味が変わるのを実感できるはずです。

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