はじめに:動的言語の限界を穿つHHVMのJITとSIMD
PHPからの脱却と静的型システムへの進化を遂げたHack言語において、その真価は単なる「コンパイル時の安全性の獲得」にとどまらない。HHVM(HipHop Virtual Machine)のコアアーキテクチャが真に牙を剥くのは、静的型システムによって保証された型情報をJIT(Just-In-Time)コンパイラが低レイヤのハードウェア命令へと変換する瞬間である。
大規模分散システムにおいて、巨大な配列やベクトルデータ(`vec
本稿では、HHVMの内部構造、メモリレイアウト、JITパイプラインの深層を解剖し、Hackのコレクション操作がどのようにしてクロックサイクルレベルで最適化され、並列命令へと変換されるのかを解説する。
—
1. Hackコレクションのメモリレイアウト:SIMD化の前提条件
SIMD命令(例えば256bitのAVX2レジスタを用いた8個の32bit整数、あるいは4個の64bit整数の同時演算)を発行するための絶対条件は、「メモリの連続性(Contiguity)」と「データ構造の予測可能性(Predictability)」である。
HHVMにおいて、Hackの主要な配列構造体である `vec
[ PackedArray Header ]
├── m_size : uint32_t (要素数)
├── m_refCount : RefCount (参照カウント)
└── m_data : TypedValue[] (連続メモリ領域)
├── TypedValue[0]: [ Value (8 bytes) | DataType (4 bytes) | Aux (4 bytes) ]
├── TypedValue[1]: [ Value (8 bytes) | DataType (4 bytes) | Aux (4 bytes) ]
└── …
通常、HHVMの `TypedValue` は16バイトのサイズを持つ。データ値(8バイト)と型識別子(`DataType`)が対になっている。このままでは純粋なスカラー値の連続配列(例:`int64_t[]`)に対して16バイトのストライド(跨ぎ)が発生し、メモリ帯域の浪費とSIMDパッキングの阻害要因となる。
しかし、HHVM JITは以下の2つのアプローチでこれを克服する:
1. Type Proof(型の完全証明): 静的型解析およびJITのGuardヒューリスティクスにより、`vec
2. Unrolled Memory Striding / Packed Vector Loading: 16バイトストライドの `TypedValue` 構造体群から、データ部(8バイトオフセット)のみを効率的にSIMDレジスタ(YMM/ZMM)へ集約(Gather/Gather-less Load)するコードパターンをVasmレイヤで生成する。
—
2. HHVM JITパイプラインにおける自動ベクトライザのメカニズム
HackのソースコードがSIMD命令に化けるまでには、4つのコンパイルフェーズを通過する。
[ Hack Source Code ]
│
▼ (HHC / Hack Compiler)
[ Hack Bytecode (HHBC) ]
│
▼ (HHVM JIT Translation Phase)
[ HHIR (High-Level Intermediate Representation) ]
│ ├── Type Guard Insertion & Hoisting
│ ├── Refcount Elision (参照カウント削除)
│ └── Bounds Check Elimination (境界チェック除去)
▼ (HHIR Optimizer / Vectorizer)
[ Vasm (Virtual Assembly – Lowering Phase) ]
│ ├── Loop Unrolling
│ └── SIMD Register Allocation (YMM0 – YMM15)
▼
[ Machine Code (AVX2 / AVX-512 Native Code) ]
ステップ1: HHIRにおけるループ不変量の抽出とガードの破棄
バイトコードから変換された初期のHHIRには、配列アクセスごとに「型チェック(`CheckType`)」および「境界チェック(`CheckBounds`)」が含まれている。
自動ベクトライザが起動するためには、JITは以下の最適化を完了しなければならない。
- Bounds Check Elimination (BCE): ループのインデックス推移(`0` から `C\count($vec)` まで)を解析し、範囲外アクセスの可能性がゼロであることを数学的に証明する。これにより、ループ内の例外送出パス(Side-Exit)を排除する。
- Refcount Elision: Hackのコレクションは値セマンティクス(Copy-On-Write)を持つが、読み取り専用のループ内では `IncRef` / `DecRef` を完全に除去する。不変性が証明されたメモリ領域は、C言語の純粋なポインタと同等に扱われる。
ステップ2: Vasmにおけるループの展開とSIMD命令への展開
HHIRからVasmへとロワーリング(Lowering)される段階で、JITのループ最適化パスはスカラーループを以下のように変換する。
1. Prolog (前処理): アライメントの確認と、SIMDベクトル幅に満たない端数処理用ループの分離。
2. Vector Loop (メインループ): ループ本体を4倍、8倍に展開(Unroll)し、AVX2命令群へマッピング。
3. Epilog (後処理): 端数要素のスカラー演算。
—
3. 実践:Hackコードと生成されるAVX2アセンブリの徹底解剖
実際のHackコードを用いて、HHVMがどのようなマシンコードを吐き出すかを検証する。
実装例:64bit整数ベクトルの要素別スケーリング演算
以下のコードは、`vec
namespace Hack\Compiler\Excellence;
<<__NEVER_INLINE>>
function scale_vector(vec
$result = vec[];
// HHVM JITはこのループ構造を検知し、
// $data が単一の型(int64)で充填されていることを前提にベクトル化を試みる
foreach ($data as $val) {
$result[] = ($val $factor) + $offset;
}
return $result;
}
HHVM JITが吐き出すVasm / x86-64 Native Assembly (AVX2最適化後)
上記ループの核心部分に対し、HHVM JIT(AVX2有効時)は以下と同等のネイティブマシンコードを生成する。
; ==============================================================================
; HHVM JIT Generated Native Assembly for scale_vector (Hot Loop Body)
; Register Allocation:
; RSI: Ptr to $data->m_data (TypedValue array)
; RDI: Ptr to $result->m_data
; YMM0: Broadcasted $factor ([factor, factor, factor, factor])
; YMM1: Broadcasted $offset ([offset, offset, offset, offset])
; RCX: Loop Index / Element Counter
; ==============================================================================
.LOOP_AVX2_VECTOR_HEADER:
; 1. 256bit (64bit integer x 4) を TypedValue 構造体から並列ロード
; TypedValue は 16バイト構造体のため、ストライドを考慮してロードする
; メモリレイアウト: [Val0, Type0, Val1, Type1]
; TypedValue[0..3] のデータ部(8バイトごとに分散)をYMM2, YMM3にギャザーロード
; ※HHVMは特定のレイアウトにおいて、非連続ロードを最小化するため、
; 以下のように128bit単位でロードし、pack/unpack処理を行う。
vmovdqu xmm2, [rsi + rcx1 – 0x10] ; Val0 + Type0
vinserti128 ymm2, ymm2, [rsi + rcx1 + 0x10], 0x1 ; Val1 + Type1 をYMMの上位へ
; (中略: インターリーブされたDataTypeタグの排除と純粋データの抽出)
; YMM4 に [Val0, Val1, Val2, Val3] の 64bit整数4つが集約完了
.LOOP_AVX2_MATH_CORE:
; 2. AVX2 ベクトル乗算 (64bit整数乗算: vpmullq はAVX-512Fだが、AVX2では32bit展開またはvpmuldq活用)
; HHVMは64bit乗算において、32bitの下位・上位積を組み合わせる高効率なSIMDシーケンスを発行する
vpmuludq ymm5, ymm4, ymm0 ; 下位32bit積の並列計算
vpsrlq ymm6, ymm4, 0x20
vpmuludq ymm6, ymm6, ymm0 ; 上位32bit積の並列計算
vpsllq ymm6, ymm6, 0x20
vpaddq ymm5, ymm5, ymm6 ; 64bit乗算結果の再構築 (ymm5 = $val $factor)
; 3. AVX2 ベクトル加算 (64bit integer x 4 の一括加算)
vpaddq ymm5, ymm5, ymm1 ; ymm5 = ymm5 + $offset
; 4. 結果を $result (TypedValue配列) へ高速書き込み
; 書き込み先のアロケーション領域に対し、DataType::Int64 (0x0A) のタグを並列付与
; (書き込み用SIMDレジスタのストア)
vmovdqu [rdi + rcx2 – 0x20], ymm5 ; データと型情報を含むバッファへのストア
; Loop Control
add rcx, 0x40 ; TypedValue 4個分 (16bytes 4 = 64bytes) インクリメント
cmp rcx, r8 ; ループ終端(要素数 16)との比較
jl .LOOP_AVX2_VECTOR_HEADER ; 継続条件が真ならループ
.LOOP_SCALAR_TAIL:
; 5. SIMDアライメントに満たない残存要素(1〜3個)のスカラー処理パス
; (Side-Exitおよびループエピローグ)
コード解説と低レイヤ分析
1. `vpaddq` / `vpmuludq` の採択: x86-64の標準スカラーレジスタ(`rax`, `rbx` 等)では、1つの加算に1クロックサイクルを要するが、AVX2の256bitレジスタ(YMM)を使用することで、1クロックサイクルあたり4つの64bit整数演算を同時に遂行する。
2. Tag Insertionの最適化: JITは、書き込み先配列 `$result` の要素型が全て `int` になることを予測(Speculate)しているため、`DataType::Int64` のビットパターン(HHVM内部表現)を定数化し、SIMDレジスタを用いてメモリへ一斉に書き込む。スカラーな `mov` 命令を1本ずつ叩く野暮な処理は完全に排除されている。
—
4. JIT自動ベクトライザを破壊するアンチパターンと回避策
どれほど精緻なHHVM JITであっても、Hackコードの書き方次第ではベクトライザが挫折し、重厚なスカラーフォールバック(Deoptimization / Side-Exit)が発生する。シニアエンジニアが把握すべき「JITを失速させないための絶対原則」を示す。
① 配列の「動的型汚染(Polymorphic Array)」
// ✕ BAD: 配列内に複数の型が混在する可能性があるコード
function bad_polymorphic(vec
$out = vec[];
foreach ($data as $item) {
if ($item is int) { // ループ内での型ガード分岐
$out[] = $item 2;
}
}
return $out;
}
- 理由: ループ内部に `is int` (`CheckType`)の条件分岐が存在するため、JITはSIMDのストライド幅を固定できず、ループのベクトル化を諦めて標準スカラーJIT(またはインタプリタへフォールバック)を選択する。
- 対策: 入力型は厳密に `vec
` を指定し、関数境界で型チェッカーに不変性を証明させること。
② ループ内でのCopy-On-Write(COW)のトリガー
// ✕ BAD: ループ内部で外部参照を持つ配列の変更
function bad_cow(vec
foreach ($data as $i => $val) {
// 他の場所で $data が参照されている場合、ここでCOWが発生し
// JITはメモリの再割当て(malloc/Arena)と全要素のコピーを発行せざるを得ない
$data[$i] = $val + 1;
}
}
- 理由: `RefCount > 1` の状態で配列要素を変更すると、JITはヒープの再割り当て関数(`hiphop_malloc`)へのC++コールを差し込まざるを得ず、SIMDレジスタの状態が破壊(Register Spill)される。
- 対策: 不変思想に基づき、新しい `vec` をアロケートするか、局所的なスコープ内で参照カウントが完全に `1` であることをJITに確信させる構造にする。
—
5. チーフアーキテクトの結論:静的型システムが解き放つハードウェアの力
HHVMのJITコンパイラにおけるSIMD自動生成メカニズムは、「優れた静的型システムは、単にバグを防ぐだけでなく、最高の性能を引き出すためのメタデータである」 というコンパイラ理論の真理を実証している。
Hack言語の型チェッカーが静的に「このコレクションは `vec
低レイヤのメモリレイアウトを意識し、JITの型推論とベクトライザが最も効率良く稼働するコードを書くこと――それこそが、HHVMとHackの性能を極限まで引き出すキーテクノロジーである。