PHP 8.x Union/Intersection Typesの内部型チェック:Zend VMが支払う「実行時コスト」の真実
コードレビュー中、若いエンジニアがドヤ顔でこんなコードを持ってきたとしよう。
// 一見すると非常に堅牢で美しく見えるドメインモデルの定義
public function process(\App\Contracts\Loggable&\App\Contracts\Renderable|string $payload): int|string|null
{
// …
}
「おっ、Union Types (`|`) と Intersection Types (`&`) を駆使して型安全性を高めました!」と彼は胸を張る。しかし、私はそのコードを見て即座にこう返す。「この型定義、本当に毎リクエスト数万回も実行されるホットパスに必要なのか? Zend VMが裏でどれだけのペナルティを払っているか分かっているのか?」と。
PHP 8の登場により、我々の型システムは劇的に表現力を増した。しかし、静的な言語(C++やJavaなど)のそれとは異なり、PHPは動的言語である。JITが有効であっても、基本的にはZend VMが実行時に型を解決し、検証し続けなければならない。
今回は、PHP 8.xにおけるUnion TypesとIntersection Typesが、Zend VMの内部(オペコードとメモリ構造)でどのように扱われ、どのような実行時コストを生んでいるのかを、低レイヤの視点から丸裸にする。
—
1. Zend VMの視点:型チェックは「タダ」ではない
PHPのソースコードは、レキサーとパーサーによってAST(抽象構文木)に変換され、最終的にZend VMが解釈・実行する「オペコード(Opcode)」へとコンパイルされる。
単なるスカラー型(`int`, `string`など)や単一のクラス型であれば、Zend VMはオペコードの実行時(Execute時)に、シンボルテーブルやオブジェクトのハンドラをわずか数クロックで比較・検証できる。C言語レベルでいえば、ポインタの比較やビットフラグのチェックに近い。
しかし、これが `A|B`(Union)や `X&Y`(Intersection)になると話は別だ。
Union Types の内部解決メカニズム
Union Typesの引数やプロパティに値が渡されたとき、Zend VMは定義された型リストの先頭から順に「マッチするかどうか」の評価を試みる(Type Hintingの解決ロジック `zend_type_list` の走査)。
- スカラー同士のUnion(例: `int|string`)であれば、型タグのチェック分岐が数回で済むためオーバーヘッドは比較的小さい。
- しかし、オブジェクトのUnion(例: `ServiceA|ServiceB|ServiceC`)が混ざると、インスタンスの継承ツリーやインターフェースの実装確認を動的に行うため、キャッシュが効かない限り、実行時のコストは跳ね上がる。
Intersection Types の冷酷な現実
Intersection Types(`X&Y`)はさらに厄介だ。これは「指定されたすべての型を同時に満たしていること」を保証しなければならない。
Zend VMは、渡された値がオブジェクトであるかを確認した上で、そのオブジェクトが持つすべてのインターフェース・クラスエントリ(`zend_class_entry`)のビットマップや継承関係を走査し、「すべての条件を満たしているか」の論理積(AND)を毎回の呼び出しで評価する。
もし、この複雑な型チェックが、フレームワークのルーティングや、数千件のレコードを処理するORMのハイドレーション(水和)処理といった「ホットパス(高頻度で実行されるコードブロック)」に存在したらどうなるか? CPUキャッシュミスが増加し、アプリケーション全体のスループットが確実に低下する。
—
2. 実務で踏み抜く「危険な設計」と正しい処方箋
では、Union / Intersection Typesは使わないほうがいいのか? 答えは「No」だ。ドメインの境界(DTOやAPIのリクエストバリデーション層)では強力な武器になる。問題なのは、「どこで使うべきか」のトレードオフを無視して、あらゆる場所に複雑な型を散りばめることだ。
以下に、実務で安全に、かつ高速に動作させるための設計パターンを示す。
コピペで使える:過剰な型複雑性を排除した堅牢なリファレンス実装
declare(strict_types=1);
namespace App\Core;
/
- 【テクニカルリードの設計指針】
- ホットパス(高頻度実行領域)での複雑なUnion/Intersectionを避け、
- 境界線(DTO/Input層)で事前に型を「正規化」する設計アプローチ。
/
// 1. 境界を定義するインターフェース
interface Renderable {
public function render(): string;
}
interface Loggable {
public function getLogMessage(): string;
}
// 2. 厳格なドメインモデル(内部処理用)
final class AuditRecord implements Renderable, Loggable
{
public function __construct(
private readonly string $message,
private readonly int $timestamp
) {}
public function render(): string
{
return “[{$this->timestamp}] {$this->message}”;
}
public function getLogMessage(): string
{
return $this->message;
}
}
/
- 3. 【アンチパターン】
- ホットパスで毎回この複雑なチェックを行わせるのはZend VMへの暴力である。
- public function handle(Renderable&Loggable|string $input): void
/
// 【推奨アプローチ】 入力層で型を解決(ポリモーフィズムの活用)
final class PayloadResolver
{
/
- 境界で一度だけ重い型チェック(またはインスタンス生成)を行い、
- 内部のビジネスロジックには「単一の明確な型」を渡す。
- @param Renderable&Loggable|string $rawPayload
/
public static function normalize(mixed $rawPayload): Renderable&Loggable
{
// 文字列で渡された場合は、内部で一度だけDTOにコンバートする
if (is_string($rawPayload)) {
return new AuditRecord($rawPayload, time());
}
// すでに条件を満たしている場合はそのまま返す
// Zend VMの実行時型チェックを最小限に抑える
if ($rawPayload instanceof Renderable && $rawPayload instanceof Loggable) {
return $rawPayload;
}
// 型が不正な場合は、早期に例外をスローして実行を止める
throw new \InvalidArgumentException(
‘ペイロードは Renderable かつ Loggable であるか、文字列である必要があります。’
);
}
}
// — 実際のアプリケーション実行コンテキスト —
try {
// 外部からの入力を受け取る(ここでのコストは許容範囲)
$input = “システム正常稼働”;
// 1. 境界で一度だけ型を安全に解決(Normalization)
$safePayload = PayloadResolver::normalize($input);
// 2. 以降のホットパスでは、Zend VMに無駄な型チェックをさせない
// ($safePayload は確実に単一の契約保証されたオブジェクトとして扱える)
echo $safePayload->render() . PHP_EOL;
} catch (\InvalidArgumentException $e) {
// ログ記録と適切なエラーレスポンス
error_log($e->getMessage());
}
—
3. アーキテクトからの提言:OPcacheと型推論の限界
PHP 8.2以降、OPcacheの最適化は進んでおり、静的に確定できる型については、バイトコード生成時にいくつかの最適化(Type Specialization)が行われる。しかし、コンパイル時に型が完全に決まらない動的なUnion / Intersectionは、どうしてもランタイムコスト(実行時検証)がつきまとう。
FPMプロセスが1秒間に何千ものリクエストを捌く高負荷なWebシステムにおいて、この「わずかなランタイムオーバーヘッドの積み重ね」が、CPU使用率のスパイクやレイテンシの増大という形で確実に牙をむく。
コードをレビューするとき、単に「動くかどうか」「型定義がエレガントかどうか」を見るだけであってはならない。
「このコードがZend VM上でどのようにオペコードに落ち、どの程度のメモリ走査コストを毎リクエスト発生させているか」を脳内でアセンブルできるようになれ。それこそが、PHPの限界を引き出す真のエンジニアリングである。