こんにちは。日々のシステム開発、本当にお疲れ様です。
PHPを使っていて、「数万件のレコードを持つ巨大なオブジェクトや配列をキャッシュから復元した途端、CPU使用率が跳ね上がり、レスポンスが急激に悪化した」という経験はありませんか?
「コードのアルゴリズムは間違っていないはずなのに、なぜかプロファイラで見るとCPUのクロックが無駄に消費されている……」
もしあなたが今、そんな壁にぶつかっているなら、それはPHPのコードレベルではなく、「メモリレイアウトとCPUキャッシュ」というハードウェアの物理法則に原因があります。
今回は、Webシステムで頻繁に使われる「JSON」と、バイナリシリアライザーの代表格「igbinary」を取り上げ、巨大なデータのデシリアライズ(復元)がPHPの内部エンジン(Zend VM)とCPUキャッシュにどのような負荷をかけているのか、その裏側の真実を紐解いていきましょう。
ここを理解すれば、PHPの裏側が驚くほど綺麗に見えるようになりますよ。
—
1. なぜ「JSON」のデシリアライズはCPUキャッシュを汚すのか?
私たちが日常的に使っている `json_decode()`。非常に便利で安全ですが、メモリとCPUの観点から見ると、実は「CPUキャッシュにとっての悪夢」のような挙動をしています。
テキストパースという重労働
JSONは本質的に「文字列(テキスト)」です。Zendエンジンが `json_decode()` を呼び出した瞬間、C言語レベルのパーサが動いて文字列をスキャンし、メモリ上の値を構築し始めます。
ここで発生する最大の問題は、「データの位置関係とメモリ上の配置がバラバラになりやすい」ということです。
JSONのパース処理では、文字列から数値を抽出し、ハッシュテーブル(PHPの `array` の実体)の動的なスロットに次々とポインタを割り当てていきます。このとき、ヒープメモリ上でのデータ配置は、CPUのL1/L2キャッシュのライン(通常64バイト)を全く意識していません。
キャッシュミスの連鎖
CPUがメインメモリ(DRAM)からデータをフェッチする速度は、L1キャッシュに比べて数十倍から数百倍も遅いです。
JSONをデシリアライズした直後の巨大な配列を走査(ループ処理など)しようとすると、CPUは「次に処理すべきデータ」を求めてメインメモリへ何度もアクセスしに行きます。これがキャッシュミス(Cache Miss)です。
CPUの演算ユニットは、データが届くまで待ちぼうけを食らうことになります。これが、「コードはシンプルなのに、なぜかCPUが非効率に動く」本当の理由です。
—
2. igbinaryの真価:メモリ連続性と構造化の美しさ
一方で、C言語の構造体をそのままシリアライズするような設計思想で作られた `igbinary` は、アプローチがまったく異なります。
igbinaryは、PHPの配列やオブジェクトを「コンパクトなバイナリ構造」に変換します。
キー名(プロパティ名など)を一度だけハッシュ化して数値IDに置き換え、実際の値データを隙間なく連続したメモリ領域に詰め込んでいくのです。
メモリの「連続性」がもたらす恩恵
igbinaryでシリアライズされたデータをデシリアライズすると、Zendエンジンの内部データ構造(`zval`)が、非常に綺麗なメモリの連続性を保ったまま復元されます。
想像してみてください。
本棚の本がバラバラの階にちらばっているのがJSONだとすれば、igbinaryは「1つの箱に、読む順番通りに隙間なく文庫本が並べられている状態」です。
CPUが配列を走査する際、最初のデータがL1キャッシュに読み込まれると、その周囲にある次のデータも「プリフェッチ機構」によって自動的に高速なキャッシュメモリへと読み込まれます。
結果としてキャッシュミスが激減し、CPUは待ち時間なしで猛烈な速度でデータを処理できるようになります。
—
3. 実践:巨大オブジェクトのメモリ効率を比較する
百聞は一見に如かず。実際にコードを書いて、この挙動の違いをイメージしてみましょう。ここでは、数万件のネストされたデータを持つ構造を想定します。
/
// 1. テスト用の巨大な連想配列(オブジェクトの代替)を生成する
$largeData = [];
for ($i = 0; $i < 50000; $i++) {
$largeData["item_" . $i] = [
'id' => $i,
‘name’ => ‘Product Name ‘ . $i,
‘price’ => mt_rand(100, 10000),
‘metadata’ => [
‘category_id’ => mt_rand(1, 10),
‘stock’ => true,
]
];
}
// — JSON方式 —
$timeStart = microtime(true);
$jsonString = json_encode($largeData);
$jsonRestored = json_decode($jsonString, true); // 配列として復元
$jsonTime = microtime(true) – $timeStart;
$jsonMemory = strlen($jsonString);
// — igbinary方式 —
$timeStart = microtime(true);
$binaryString = igbinary_serialize($largeData);
$binaryRestored = igbinary_unserialize($binaryString);
$binaryTime = microtime(true) – $timeStart;
$binaryMemory = strlen($binaryString);
// 結果出力
echo “=== パフォーマンス比較 (50,000要素) ===\n”;
echo sprintf(“JSON : 処理時間 %.4f秒 / データサイズ %.2f MB\n”, $jsonTime, $jsonMemory / 1024 / 1024);
echo sprintf(“igbinary : 処理時間 %.4f秒 / データサイズ %.2f MB\n”, $binaryTime, $binaryMemory / 1024 / 1024);
このコードが語るエンジニアリングの真実
手元でこのコードを走らせてみると分かりますが、`igbinary` はデータサイズがJSONに比べて劇的に小さくなります(おおよそ1/3〜1/5程度になることも珍しくありません)。
なぜサイズが小さくなるのか?
それは、JSONのようにキー名(`”id”`, `”name”`, `”price”`など)を毎回文字列として保持せず、内部的なID参照(整数)に置き換えているからです。
データサイズが小さいということは、RedisやMemcachedなどの外部キャッシュサーバーとのネットワーク転送量(帯域幅)も同時に削減できることを意味します。Webシステムのスケールにおいて、これは強力な武器になります。
—
4. アーキテクトとして知っておくべき選定の基準
「じゃあ、すべてのシリアライズをigbinaryに置き換えれば完璧ですね?」という声が聞こえてきそうですが、そこは世界最高峰のWebシステムを目指すアーキテクト。トレードオフを忘れてはいけません。
1. 外部システムとの連携(JSONの優位性)
JSONは人間が読めるテキストであり、言語やプラットフォームを問いません。例えば、PHPで生成したキャッシュをNode.jsやPython、あるいはフロントエンドのJavaScriptで直接扱いたい場合、igbinaryは無力です(バイナリであるため、他言語でデコードするには専用のライブラリやパース定義が必要です)。
- JSONを使うべき場面: APIレスポンス、他言語サービス間連携、デバッグが容易であるべき設定ファイルなど。
2. 内部キャッシュと高負荷処理(igbinaryの優位性)
PHPからPHPへの高速なデータ受け渡し(Redisへのセッション保存や、巨大なドメインモデル・オブジェクトグラフのキャッシュなど)においては、igbinaryの独壇場です。CPUキャッシュ効率とメモリ節約の恩恵を最大に受けられます。
- igbinaryを使うべき場面: Redis/Memcachedに保存する内部キャッシュ、モノリスなPHPアプリケーション内のセッションストレージ、ジョブキュー(Laravel Queue等)のペイロード。
—
まとめ:ハードウェアを意識したコードを書く
今回のテーマをまとめます。
1. JSONデシリアライズはテキストパースの負荷が高く、メモリ配置が散らばるため、CPUのキャッシュミス(L1/L2 Cache Miss)を引き起こしやすい。
2. igbinaryはメモリの連続性が高くコンパクトなため、CPUキャッシュ効率が劇的に向上し、結果として全体のスループットが向上する。
3. 用途に応じてJSONとバイナリを使い分けることが、プロフェッショナルなPHPアーキテクチャの設計思想である。
PHPは「簡単に書ける言語」として広く普及しましたが、その内部(Zend VM、メモリ管理、ハッシュテーブルの挙動)を知ることで、エンタープライズ領域でも戦える超高速なWebシステムを構築できます。
あなたの書くコードが、ハードウェアの特性を最大限に活かし、美しく高速に動作することを、私は心から応援しています。