こんにちは。PHPの裏側で何が起きているのか、気になって夜も眠れない時期ってありますよね。
世の中の解説記事を見ると、「Fiberを使えば非同期処理が簡単に書けますよ」「コールバック地獄から解放されますよ」といった、表面的なメリットばかりが語られがちです。しかし、我々Webシステムアーキテクトが向き合うべきなのは、そのコードがZend Engineのメモリ空間とCPUの物理レイヤでどのような暴力を振るっているのかという現実です。
今回は、PHP 8.1で導入された「Fiber(ファイバー)」を取り上げます。独自のスタックを持つこの仕組みが、メモリ管理やCPUキャッシュ、そしてTLB(Translation Lookaside Buffer)にどのような影響を与え、なぜ「銀の弾丸」ではないのか。低レイヤの視点から、綺麗に紐解いていきましょう。
—
1. Fiberの正体:Zend VMにおける「ユーザー空間のスタック」
まず、PHPの通常のリクエスト処理を思い出してください。Webサーバーからリクエストを受け取ると、PHP-FPMのプロセス(あるいはSwooleなどの常駐プロセス)が動き出し、C言語のコールスタック上にZend Executorのコンテキストが展開されます。関数が呼ばれるたびにスタックフレーム(`zend_execute_data`)が積まれ、関数が終わればPOPされる。これはOSが管理する純粋なコールスタックです。
一方、Fiberはこの仕組みをユーザーランド(PHPスクリプト側)に持ち込みます。
Fiberを生成すると、Zend Engineはヒープメモリ上に専用の実行スタック(Fiber用のスタック領域)を動的に割り当てます。通常の関数呼び出しがOSやCランタイムのスタックを使うのに対し、Fiberは自前の「仮想的なスタック」の上で動くわけです。
start(‘アーキテクト’);
echo “メイン側で受け取り: {$output}\n”;
// Fiberを再開
$fiber->resume(‘再開の合図’);
このコードの裏側で何が起きているか、Zend VMの視点で見てみましょう。
`new Fiber()` が実行されたとき、エンジンはPHPのメモリマネージャー(Zend MM)を介して、ヒープ上にスタック用のメモリブロックを確保します。通常の関数コールであればCPUのハードウェアスタックポインタ(RSPレジスタなど)を少し動かすだけで済むところを、Fiberでは「どのメモリ領域をスタックとして使うか」を明示的に切り替えるコンテキストスイッチの準備が行われます。
—
2. コンテキストスイッチがCPUキャッシュに与える致命傷
「非同期処理でI/O待ちを効率化できるなら、何でもFiberに載せればいいじゃないか」と思ってしまいがちですが、ここに大きな罠があります。それがCPUキャッシュ(L1/L2/L3)のコールド化です。
CPUは、メモリ(DRAM)へのアクセスが遅いため、直近でアクセスしたデータや命令を高速なSRAMであるキャッシュメモリに保持しています。
1. メインの処理が走る:CPUのL1/L2キャッシュには、Webフレームワークのルーティング処理やORMのコード、オブジェクトのプロパティがぎっしり詰まっています。
2. Fiberにスイッチする:スタックポインタがガラリと変わり、実行アドレスが全く別のヒープ領域(Fiber内の処理)にジャンプします。
3. キャッシュミス(Cache Miss)の発生:CPUから見ると、突然「今まで使っていたものとは全く違うメモリ空間」へ飛ばされたことになります。これにより、L1/L2キャッシュに載っていたデータが無効化され、CPUは低速なメインメモリへデータを読みに行かざるを得なくなります(キャッシュのコールド化)。
つまり、細かすぎるFiberのスイッチングは、CPUキャッシュヒット率を劇的に低下させ、かえってスループットを悪化させる原因になります。これが、CPUアーキテクトが「コンテキストスイッチはタダではない」と口を酸っぱくして言う理由です。
—
3. TLB(Translation Lookaside Buffer)フラッシュの脅威
CPUキャッシュ以上に厄介なのが、TLB(Translation Lookaside Buffer)への影響です。
我々が書くPHPコード上のメモリ番地(仮想アドレス)は、CPUのMMU(Memory Management Unit)によって物理メモリ上の番地に変換されています。この変換テーブル(ページテーブル)のキャッシュを保持しているのがTLBです。
Fiberが独自のスタックをヒープ上に大量に確保し、それらがメモリ上で散らばっている(フラグメンテーションを起こしている)場合、Fiber間でコンテキストスイッチが頻発するとどうなるでしょうか。
- アクセスする仮想アドレス空間の範囲が広がり、TLBに収まりきらなくなります。
- 結果としてTLBミス(TLB Miss)が頻発し、CPUはページテーブルを参照するためにメモリへ追加のアクセスを行うことになります(Page Walkの発生)。
高トラフィックなWebアプリケーションにおいて、このTLBミスとCPUキャッシュのコールド化が積み重なると、CPUのサイクルの大部分が「メモリの待ち時間」に消えていくことになります。
—
4. 実務でFiberを設計する際の「極意」
では、私たちはこの強力なFiberというおもちゃを、実務でどのように扱うべきなのでしょうか。答えはシンプルです。「CPUバウンドな処理には使わず、重いI/Oバウンドな境界でのみ使う」ということです。
データベースのクエリ待ち、外部APIのHTTPリクエスト待ちといった「CPUが何もすることがなくて暇をしている時間(I/O Wait)」の間だけFiberに処理を譲る(Suspendする)のであれば、コンテキストスイッチのオーバーヘッドは、I/O待ちの圧倒的な時間的メリットによって完全に相殺されます。
逆に、CPUでゴリゴリ計算するようなロジックや、細かすぎる非同期化にFiberを使うのは、エンジンの寿命を縮める(CPUを無駄に空回りさせる)行為に他なりません。
実践的な非同期I/Oモックのイメージ
fibers[$key] = new Fiber(function () use ($task) {
// タスクを実行(内部でI/O待ちが発生することを想定)
$result = $task();
Fiber::suspend($result);
});
}
public function run(): array {
$results = [];
// 全てのFiberを起動
foreach ($this->fibers as $key => $fiber) {
$results[$key] = $fiber->start();
}
// ここで実際のイベントループやI/O多重化(stream_select等)と連携させる
// 実際のプロダクションでは Amp や ReactPHP などのエコシステムがこれを担います
return $results;
}
}
まとめ
PHPのFiberは、ユーザーランドに協調的マルチタスクをもたらした画期的な機能です。しかし、それは魔法の杖ではありません。
- Fiberはヒープ上に独自のスタック領域を確保する。
- 頻繁なコンテキストスイッチは、CPUキャッシュのコールド化とTLBミスを引き起こす。
- だからこそ、「どこでスイッチし、どこでCPUを集中させるか」の設計思想が問われる。
この裏側のメカニズムを知っていれば、ネット上の浅い情報に振り回されることなく、「このアーキテクチャでFiberを使うべきか、あるいは素直にPHP-FPMのプロセスモデルに任せるべきか」を自信を持って判断できるようになります。
PHPの底は、まだまだ深く、そして美しい。
次回のコードレビューでは、ぜひこの「キャッシュとメモリ空間の動き」を脳内トレースしながら設計に臨んでみてください。世界が変わって見えるはずです。