【実務・中級編】FiberとGeneratorの比較:協調的マルチタスクにおけるパフォーマンス特性とユースケースの境界線 – PHPコア・内部エンジンと高速化・並行処理の極意解析バイブル

FiberとGeneratorの比較:協調的マルチタスクにおけるパフォーマンス特性とユースケースの境界線

コードレビューをしていて、未だに「非同期処理=多重スレッドやマルチプロセス、あるいは面倒なコールバック地獄」という古いパラダイムに囚われたコードを見かけることがある。しかし、現代のPHP(PHP 8.1以降)において、I/Oバウンドなボトルネックを優雅に粉砕する武器は既に標準搭載されている。それが Fiber(ファイバー) だ。

一方で、PHP 5.5から我々を支え続けてきた Generator(ジェネレータ) もまた、協調的マルチタスク(Cooperative Multitasking)の文脈で語られることが多い。では、この2つは一体何が異なり、どのような基準で使い分けるべきなのか。Zend VMの内部挙動とメモリフットプリントの観点から、その境界線を鮮やかに引いてみせよう。

—

1. Zend VM内部におけるGeneratorとFiberの決定的な違い

まず、言語仕様の表面的な機能ではなく、PHPの心臓部であるZend Engineがそれらをどう扱っているのかを低レイヤから理解する必要がある。

Generator:スタックを持たない「片方向の脱出装置」

Generatorは、関数が途中で処理を中断(`yield`)し、呼び出し元に値を返した後、再び同じ位置から再開できる機能だ。しかし、Zend VMの構造上、Generatorは独自のコールスタックを持たない。
Generatorが保持するのは、ローカル変数の状態を保持する専用のコンテキスト(`zend_generator`構造体)と、実行中のオペコード(Opcodes)のポインタのみである。

そのため、Generatorの制御フローは常に「親子関係」に縛られる。呼び出し元(Caller)から呼び出され先(Callee)へ制御は移るが、深いコールツリーの途中で突然別のコンテキストへジャンプするような「非対称コルーチン(Asymmetric Coroutine)」としての自由度はない。メモリ効率は極めて高いが、表現力には明確な限界がある。

Fiber:真の「サスペンド可能なコールスタック」

それに対し、FiberはPHP 8.1で導入された、真の意味でのファイバー(軽量スレッド)である。
Fiberの本質は、独自のCスタック(正確にはZend VMの実行スタック構造体である `zend_execute_data` のチェーン)をヒープ上に確保・退避できる点にある。

これにより、深いネストを持った関数呼び出しのどこからでも、`Fiber::suspend()` を叩くだけで実行コンテキストを即座に中断し、イベントループや別のスケジューラへ制御を明け渡すことができる。そして、外部から `Fiber::resume()` が呼ばれた瞬間、中断したまさにその深淵なコールスタックの地点から処理が復元される。

これが意味するのは、「コールスタックのどこからでも非同期化のフックを差し込める」という圧倒的な表現力だ。

—

2. パフォーマンス特性とメモリオーバーヘッドの比較

「Fiberの方が高機能なら、全部Fiberで作ればいいのではないか?」という短絡的な思考は、コードレビューで一刀両断する。パフォーマンスとメモリのトレードオフをデータ構造から紐解こう。

| 評価軸 | Generator | Fiber |
| :— | :— | :— |
| メモリ消費量 | 非常に軽量(数キロバイト程度) | やや重い(スタック管理用構造体のオーバーヘッドあり) |
| スタック管理 | なし(単一のコールスタック上で状態を維持) | あり(独立した実行コンテキスト・スタックを保持) |
| 主な用途 | 大規模データの遅延評価、メモリ効率の良いイテレーション | 非同期I/O、イベントループ駆動型のコルーチン制御 |
| 制御の複雑さ | 低〜中(直感的なシーケンシャル記述が可能) | 中〜高(イベントループやスケジューラとの統合が必須) |

ベンチマークを取ると、単なる数万件のループ処理やデータのストリーミングにおいて、Fiberを乱用することはメモリ帯域とCPUキャッシュの観点から悪手であることが分かる。Fiberはコンテキストスイッチの際にスタックの退避・復元コストが発生するため、純粋なイテレーション速度ではGeneratorの足元にも及ばない。

—

3. 実務におけるユースケースの境界線

実務のシステムアーキテクチャ設計において、この2つをどう棲み分けるべきか。境界線は極めて明確だ。

  • Generatorを使うべき領域:
  • 数百万件のDBレコードや巨大なログファイルを、メモリを溢れさせずに順次処理(ストリーミング)する。
  • 無限シーケンスの生成や、パイプライン処理によるデータの遅延評価。
  • Fiberを使うべき領域:
  • 非同期HTTPクライアントやWebSocketサーバなど、イベントループ(AmpやReactPHPなどのベース)上で複数のI/O待ち処理を並行実行する。
  • 「同期的なコードスタイルを維持したまま、内部で非同期I/Oを待ち合わせたい」というユースケース。

—

4. 【実践】Fiberを活用した協調的HTTPクライアントのミニ実装

百聞は一見にしかず。イベントループの概念を自前でごくシンプルに実装し、複数の外部APIリクエストをFiberで非同期並行処理するリファレンスコードを提示する。実務のAPIゲートウェイやマイクロサービスの通信基盤の原型として脳内に入れておいてほしい。

  • 簡易的なタスクスケジューラ(イベントループのモック)
  • 複数のFiberを管理し、I/O待ちの間に効率的にコンテキストスイッチを行う。
  • /
    class SimpleEventLoop
    {
    / @var \SplQueue /
    private \SplQueue $queue;

    public function __construct()
    {
    $this->queue = new \SplQueue();
    }

    /

    • Fiberをスケジューラに登録する

    /
    public function add(Fiber $fiber): void
    {
    $this->queue->enqueue($fiber);
    }

    /

    • 全てのFiberが完了するまでイベントループを回す

    /
    public function run(): void
    {
    while (!$this->queue->isEmpty()) {
    $fiber = $this->queue->dequeue();

    if ($fiber->isTerminated()) {
    continue;
    }

    try {
    // 初回起動、またはsuspendからの復帰
    if (! $fiber->isStarted()) {
    $fiber->start();
    } else {
    $fiber->resume();
    }

    // 実行後も終了していなければ、キューの末尾に戻す(協調的マルチタスク)
    if (! $fiber->isTerminated()) {
    $this->queue->enqueue($fiber);
    }
    } catch (\Throwable $e) {
    // 実務ではここでロギングと例外伝播のハンドリングを行うこと
    echo “Fiber内で例外が発生しました: ” . $e->getMessage() . “\n”;
    }
    }
    }
    }

    /

    • モック化された非同期HTTPリクエスト関数
    • 実務では curl_multi_ や Non-blocking Streams を用いてイベントループにバインドする。

    /
    async_http_get(string $url, SimpleEventLoop $loop): Fiber
    {
    return new Fiber(function () use ($url) {
    echo “[Fiber] リクエスト送信開始: {$url}\n”;

    // 擬似的なI/O待ち(本来はここでソケットのreadableを待つ)
    $waitTime = mt_rand(1, 3);

    // ★ここでFiberをサスペンドし、イベントループに制御を戻す
    Fiber::suspend([
    ‘url’ => $url,
    ‘wait’ => $waitTime
    ]);

    // ─── ここから下がレジューム後の処理 ───
    echo “[Fiber] レスポンス受信完了: {$url} (待機時間: {$waitTime}秒)\n”;
    return “Data from {$url}”;
    });
    }

    // ==========================================
    // 実行スクリプト
    // ==========================================

    $loop = new SimpleEventLoop();

    // 3つの異なる外部APIへ並行リクエストを送るシミュレーション
    $fiber1 = async_http_get(‘https://api.example.com/v1/users’, $loop);
    $fiber2 = async_http_get(‘https://api.example.com/v1/orders’, $loop);
    $fiber3 = async_http_get(‘https://api.example.com/v1/products’, $loop);

    // スケジューラに登録
    // (実務ではここでI/OのポーリングとFiberの再開条件を紐付けます)
    $loop->add($fiber1);
    $loop->add($fiber2);
    $loop->add($fiber3);

    $startTime = microtime(true);
    echo “=== イベントループ開始 ===\n”;
    $loop->run();
    $endTime = microtime(true);

    printf(“=== 全処理完了 (実行時間: %.2f秒) ===\n”, $endTime – $startTime);

    この設計が実務で強固である理由

    上記のコードは一見シンプルだが、重要なアーキテクチャの原則を含んでいる。
    1. ブロッキングの排除: 同期的な `sleep()` や `curl_exec()` をそのまま並べると処理時間が「全リクエストの合算」になるが、Fiberと協調的マルチタスクを組み合わせることで、「最も重い単体の処理時間(MAX値)」に近い時間で完了させることができる。
    2. メモリの安全性: 生成されたFiberはガベージコレクタ(GC)の管理下にあり、スコープを抜ければ適切にメモリが解放される。ただし、循環参照によるメモリリークには注意が必要だ。

    —

    5. テクニカルリードからの最終提言

    PHPにおける非同期処理の文脈において、Generatorは「メモリ効率を極限まで高めたデータストリーミングの主役」であり、Fiberは「I/Oバウンドなアプリケーションの構造を美しく非同期化するためのコルーチンの基盤」である。

    これらを混同し、何でもかんでもFiberでラップしようとする設計は、不要なコンテキストスイッチのオーバーヘッドを招き、パフォーマンスを逆に悪化させる。システムの要件定義の段階で、扱うデータが「メモリ上のストリーム(Generator)」なのか、「外部とのI/O待ち(Fiber)」なのかを見極め、コードをレビューの網にかけること。

    その判断ができるエンジニアこそが、Zend VMの挙動を支配し、真にスケーラブルなWebシステムを構築できるアーキテクトなのだ。

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