シリアライズの極限:JSONの呪縛から逃れ、igbinaryでZendエンジンを解放する方法
コードレビューの場で、次のようなコードを見るたびに私は深い絶望感を覚える。
// 最もよく見るが、最もメモリとCPUをドブに捨めているアンチパターン
$cache->set(‘user_session:’ . $userId, json_encode($largeObjectArray));
「JSONは人間が読めるし、どこでも動くから安全」――この思考停止こそが、高負荷なWebアプリケーションのボトルネックを生み出す最大の癌だ。数万件のオブジェクトが詰め込まれた巨大な配列を、わざわざ人間が読めるテキストに変換し、それを再びパースするためにZendエンジンのCPUサイクルを焼き尽くす。そして、背後では何ギガバイトものメモリがガベージコレクションの嵐に晒されている。
本稿では、PHPのシリアライズ方式(標準の `serialize`、`json_encode`、そしてバイナリの救世主 `igbinary`)が、内部のZendエンジンとメモリ空間(HashTable)において何を引き起こしているのかを、低レイヤの視点から徹底的に解剖する。
—
1. Zendエンジンから見たシリアライズの正体
PHPの内部データ構造は、すべて `zval`(Zend Value)というC言語の構造体と、連想配列を管理する `HashTable` によって構成されている。
JSONが抱える「3重の罪」
1. 型情報の文字列化: `123` という整数であっても、JSONにするためには `’1′ ‘2’ ‘3’` というASCII文字の列に変換され、復元時には再びCPUがこれをパース(数値変換)しなければならない。
2. キー名の重複保持: 連想配列をJSON化すると、すべての要素でキー名(文字列)がJSONペイロードに含まれる。これはネットワーク転送量だけでなく、メモリ帯域をも無駄に消費する。
3. Zendハイパーバイザのオーバーヘッド: `json_encode()` / `json_decode()` は内部的に外部ライブラリ(jsmnやonigmo等)を叩くため、Zend VMのコンテキストスイッチやメモリ確保(emalloc)のコストが発生する。
標準 `serialize()` の限界
PHP標準の `serialize()` はZendの型をそのままバイナリにするためJSONよりはマシだが、キー名やクラス名を完全に文字列として保持するため、データ構造が肥大化しやすい。
究極のバイナリフォーマット `igbinary`
`igbinary` は、Zendエンジンの内部構造に特化したシリアライザだ。
- キーのID化: 一度出現したキー名やクラス名を内部テーブルに登録し、以降は小さな整数ID(インデックス)に置き換えてバイナリ化する。
- 型情報の保持: `zval` の型情報を極めてコンパクトなバイナリとしてシリアライズするため、展開時(`igbinary_unserialize`)にCPUが文字列パースを行う必要がほとんどない。
—
2. ベンチマークとメモリ消費量の実態
百聞は一見にしかず。実務で頻出する「1万件の構造化された連想配列(ユーザーデータ・メタデータ・権限情報を含む)」を想定し、各シリアライズ方式の挙動を計測するテストコードを提示する。
以下のコードは、単なる速度比較ではない。メモリのピーク使用量(Peak Memory)とペイロードサイズに注目してほしい。
/
declare(strict_types=1);
// 1. テストデータの生成(実務を想定し、キー名が重複する巨大な配列を作成)
$mockData = [];
for ($i = 0; $i < 10000; $i++) {
$mockData[] = [
'id' => $i,
‘uuid’ => ‘a1b2c3d4-e5f6-7890-abcd-ef0123456789’,
‘username’ => ‘engineer_master_’ . $i,
‘roles’ => [‘ROLE_ADMIN’, ‘ROLE_USER’, ‘ROLE_API’],
‘metadata’ => [
‘login_count’ => 150,
‘last_ip’ => ‘192.168.1.1’,
‘preferences’ => [‘theme’ => ‘dark’, ‘notifications’ => true]
]
];
}
// 測定ヘルパー
function measure(string $label, callable $callback): void {
gc_collect_cycles();
$startMemory = memory_get_usage(true);
$startTime = hrtime(true);
$result = $callback();
$endTime = hrtime(true);
$endMemory = memory_get_usage(true);
$timeMs = ($endTime – $startTime) / 1e6;
$memoryKb = ($endMemory – $startMemory) / 1024;
echo sprintf(
“[%s]\n – ペイロードサイズ: %s バイト\n – 処理時間: %.4f ms\n – 差分メモリ: %.2f KB\n – ピークメモリ: %.2f MB\n\n”,
$label,
is_string($result) ? strlen($result) : ‘N/A’,
$timeMs,
$memoryKb,
memory_get_peak_usage(true) / 1024 / 1024
);
}
// — A. JSON (JSON_UNESCAPED_UNICODE) —
measure(‘ext-json (json_encode / json_decode)’, function () use ($mockData) {
$encoded = json_encode($mockData, JSON_UNESCAPED_UNICODE);
return json_decode($encoded, true);
});
// — B. PHP Native (serialize / unserialize) —
measure(‘Native (serialize / unserialize)’, function () use ($mockData) {
$encoded = serialize($mockData);
return unserialize($encoded);
});
// — C. igbinary (igbinary_serialize / igbinary_unserialize) —
measure(‘ext-igbinary (igbinary_serialize / igbinary_unserialize)’, function () use ($mockData) {
$encoded = igbinary_serialize($mockData);
return igbinary_unserialize($encoded);
});
このベンチマークが示す隠された真実
通常、`igbinary` を使用すると、JSONと比較して:
1. ペイロードサイズが約 40% ~ 60% 削減される(キー名がID化されるため)。
2. CPU処理時間(特にデシリアライズ時)が劇的に短縮される。
3. RedisやMemcachedなどのKVSに保存する際、ネットワークI/Oの帯域負荷が半分以下になる。
—
3. 実務で即座に使える:堅牢なキャッシュ・ストレージ抽象化クラス
それでは、この知見を実際のWebアプリケーションの設計に落とし込む。
「データの永続化先(Redisやファイル)によってシリアライズ方式を動的に切り替えつつ、環境依存(igbinaryが入っていない環境へのフォールバック)を考慮した堅牢なコンポーネント」を実装する。
このコードは、単なるラッパーではない。メモリの断片化を防ぎ、予期せぬ例外を完全にハンドリングするプロダクションクオリティの設計だ。
/
class SerializerManager
{
public const FORMAT_JSON = ‘json’;
public const FORMAT_NATIVE = ‘native’;
public const FORMAT_IGBINARY = ‘igbinary’;
private string $defaultFormat;
public function __construct(string $defaultFormat = self::FORMAT_IGBINARY)
{
$this->defaultFormat = $this->resolveBestFormat($defaultFormat);
}
/
- 環境に応じて利用可能な最適なフォーマットを決定する
/
private function resolveBestForm(string $preferred): string
{
if ($preferred === self::FORMAT_IGBINARY && !extension_loaded(‘igbinary’)) {
// igbinaryが有効でない場合は安全にネイティブシリアライズへフォールバック
return self::FORMAT_NATIVE;
}
return $preferred;
}
/
- データをシリアライズして返す
- @param mixed $data
- @return string
- @throws RuntimeException
/
public function serialize(mixed $data): string
{
try {
return match ($this->defaultFormat) {
self::FORMAT_IGBINARY => igbinary_serialize($data),
self::FORMAT_NATIVE => serialize($data),
self::FORMAT_JSON => json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
default => throw new RuntimeException(“Unsupported serialization format: {$this->defaultFormat}”),
};
} catch (Throwable $e) {
// シリアライズ失敗時の原因をコンテキストと共にラップ
throw new RuntimeException(“Failed to serialize data: ” . $e->getMessage(), 0, $e);
}
}
/
- ペイロードをデシリアライズして元の構造体を復元する
- @param string $payload
- @return mixed
- @throws RuntimeException
/
public function unserialize(string $payload): mixed
{
if ($payload === ”) {
return null;
}
try {
// ペイロードの魔術的検知(igbinaryのヘッダやJSONのフォーマットを簡易判定)
if ($this->isJson($payload)) {
return json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
}
// igbinary または native の判定
// igbinaryのデータは特定のヘッダを持つため判別可能だが、
// 安全のため例外キャッチでフォールバックさせる
if (extension_loaded(‘igbinary’)) {
$unserialized = @igbinary_unserialize($payload);
if ($unserialized !== null || $payload === igbinary_serialize(null)) {
return $unserialized;
}
}
return unserialize($payload, [‘allowed_classes’ => false]);
} catch (Throwable $e) {
throw new RuntimeException(“Failed to unserialize payload: ” . $e->getMessage(), 0, $e);
}
}
/
- 文字列がJSON形式かどうかを簡易判定
/
private function isJson(string $string): bool
{
$firstChar = $string[0] ?? ”;
return $firstChar === ‘{‘ || $firstChar === ‘[‘;
}
}
—
4. アーキテクトからの警告:デシリアライズにおけるセキュリティとメモリ破壊
最後に、シリアライズを扱う上で絶対に忘れてはならない致命的なセキュリティリスクについて言גתしておかねばならない。
PHPの標準 `unserialize()` は、悪意ある文字列を流し込まれた場合、オブジェクトインジェクション(Object Injection)と呼ばれる深刻な脆弱性を引き起こす。マジックメソッド(`__destruct` や `__wakeup`)を持つクラスが存在する場合、任意のコード実行(RCE)の踏み台にされる。
設計上の鉄則
1. 外部からの入力(ユーザーのCookie、リクエストパラメータ、信頼できないAPIレスポンス)に対して絶対に `unserialize()` や `igbinary_unserialize()` を使ってはならない。
2. データベースやKVS(Redis等)といった「自社が完全にコントロールしている信頼できるストレージ層」とのやり取りにのみ、バイナリシリアライズを使用すること。
3. 外部公開するAPIやWebhooksのデータ交換には、依然として厳格なスキーマ検証を経た `json_encode` / `json_decode`(またはProtocol Buffersなど)を採用すべきである。
結びにかえて
「動けばいい」というコードは、トラフィックが10倍になった瞬間にシステムを殺す。Zendエンジンの内部構造を想像し、メモリのアロケーションとCPUのキャッシュ効率に意識を向けること。それこそが、真にスケーラブルなPHPアプリケーションを構築する唯一の道である。
今日からあなたのプロジェクトのRedisキャッシュ層を見直し、`json_encode` の呪縛からコードベースを解放してほしい。そのパフォーマンスの向上の先に、エンジニアとしての本当の成果が待っている。