こんにちは!Hack言語の世界へようこそ。
コミッターとしてHHVM(HipHop Virtual Machine)の内部実装やJIT(Just-In-Time)コンパイラと日々格闘している先輩エンジニアです。
Hack言語の魅力は、美しい静的型システムだけでなく、「その型情報をフルに活用してHHVMが実行時にコードを超高速化してくれること」にあります。
今回は、HHVMのJITコンパイラが行う最高レベルの最適化テクニックの一つ、「ループ不変量コード移動(LICM: Loop Invariant Code Motion)」について分かりやすく解説します。
「難しそう…」と思うかもしれませんが、大丈夫!「なぜ自分が書いたプログラムがHHVM上で爆速で動くのか」という本質が、身近な例えと図解ですっきりとマスターできますよ。一緒に学んでいきましょう!
—
1. LICM(ループ不変量コード移動)ってなに?
まずは直感的なイメージから掴みましょう。
想像してみてください。あなたは「クッキーを1000個焼く作業」をループ処理で行っています。
- ダメな例: クッキーを1個丸めるたびに、毎回キッチンを出て「小麦粉の袋の重さ」を計り直しに行っている。
- 賢い例: 「小麦粉の袋の重さ」は途中で変わらないのだから、作業を始める前に1回だけ計っておく。
この「賢い例」こそが LICM(ループ不変量コード移動) です!
プログラムのループ処理の中に「何回計算しても結果が変わらないコード(=不変量: Invariant)」がある場合、HHVMのJITコンパイラはそれを自動的に検出し、ループの外(直前)へ引っ張り出して1回だけ実行するように書き換えてくれます。
視覚的なイメージ図
【最適化前:あなたの書いたコード】
ループ開始 (1000回繰り返す)
│
├── 1. 「10 + 20」を計算する (毎回結果は30なのに、1000回計算…) 💦
├── 2. 配列の要素に30を掛ける
│
ループ終了
↓ HHVM JITの魔法(LICM)
【最適化後:JITが生成した機械語】
★ 「10 + 20 = 30」を事前に1回だけ計算! (Pre-header)
│
ループ開始 (1000回繰り返す)
│
└── 配列の要素に 30 を掛ける (無駄な計算が消えた!) ✨
ループ終了
—
2. 具体的なHackコードで見てみよう
実際のHackコードを使って、どこが「ループ外に移動できる計算」なのかを見てみましょう。
// usersのスコアに、固定の倍率とボーナスを加算して計算する関数
function calculateFinalScores(
vec
int $baseMultiplier,
int $bonus
): vec
$result = vec[];
for ($i = 0; $i < C\count($scores); $i++) { // 💡 ここに注目! // $baseMultiplier 2 + $bonus はループ中で一度も変化しません。 $effectiveBonus = ($baseMultiplier 2) + $bonus; $result[] = $scores[$i] + $effectiveBonus; } return $result; }
コード解説
このコードの `$effectiveBonus = ($baseMultiplier 2) + $bonus;` は、ループが1000回回ろうが100万回回ろうが、常に同じ結果になりますよね。
HHVMのJITは、このコードを機械語にコンパイルする際、自動的に以下のような構造へ変換して実行します。
// JITが内部で行うイメージ(実際は中間表現 HHIR レベルで処理されます)
function calculateFinalScores_JIT_OPTIMIZED(…): vec
$result = vec[];
// 🚀 ループの外側(Pre-header)へ自動的に持ち上げられた(Hoistされた)計算!
$effectiveBonus = ($baseMultiplier 2) + $bonus;
$count = C\count($scores); // 実はこれも型が不変なら外に出せる!
for ($i = 0; $i < $count; $i++) { $result[] = $scores[$i] + $effectiveBonus; } return $result; } 開発者がいちいち手動で変数を外に出さなくても、JITが安全性を確認した上で勝手に超高速化してくれるのです。うれしいですね!
—
3. なぜHack言語だとLICMが超強力に動くのか?
「他の言語(PHPなど)でも同じことができるのでは?」と思った方、鋭いです!
しかし、Hack言語の静的型システムとHHVMの組み合わせだからこそ、この最適化が超強烈に機能します。
静的型システムがJITを加速させる理由
動的型言語(古いPHPなど)では、ループの途中で変数の型が急に変わったり、グローバルな副作用で値が書き換わる可能性を打ち消せません。そのためコンパイラは「安全のためにループ外へ移動させるのをやめておこう…」と弱気になってしまいます。
しかし、Hack言語には厳格な型があります。
1. 型の一貫性: `$baseMultiplier` が `int` であることが型チェッカーによって保証されている。
2. 副作用の不在: 単純な算術演算(`+` や “)には副作用がないことが一目でわかる。
3. エイリアシングの否定: メモリの別の場所を書き換えたときに、この変数が予期せず化ける心配がない(エイリアス解析)。
HHVMの中間表現(HHIR)は、この型情報を利用して「Guard(型のチェック)」をループの外側に配置します。型チェックが1回で済むからこそ、安心して不変なコードをループ外へ移動(Hoist)できるのです!
—
4. 注意!JITがLICMを「あきらめる」罠パターン
JITは非常に賢いですが、「外に持ち出すと挙動が変わってしまうリスクがあるコード」に対しては、安全第一でLICMの適用を諦めます。
初心者がやりがちな「JITの最適化を邪魔してしまうパターン」を3つ知っておきましょう。
罠①:ループ内で「副作用のある関数」を呼んでいる
function processData(vec
for ($i = 0; $i < C\count($items); $i++) {
// ❌ ログ出力やDB書き込み、乱数発生などは「副作用」があるため外に出せません!
$config = fetchConfigFromDatabase();
doSomething($items[$i], $config);
}
}
解説: `fetchConfigFromDatabase()` が毎回同じ値を返す保証はHHVMにはありません。そのため、この呼び出しはループ内に残されてしまいます。
罠②:配列やオブジェクトを毎回生成している
function processItems(vec
for ($i = 0; $i < C\count($items); $i++) {
// ⚠️ 毎回新しいDict(メモリ割り当て)が発生している
$options = dict['debug' => true, ‘version’ => 1];
execute($items[$i], $options);
}
}
解説: オブジェクトやコレクションの新規作成(メモリ割り当てや参照カウンタの操作)は「メモリ状態を変える副作用」とみなされ、LICMで外に出すのが難しくなります。(※非常に単純なケースでは逃げ水解析で最適化されることもありますが、外に出しておくのが無難です)。
—
5. JITに愛される「美しく高速なHackコード」を書く3つの心得
ここをクリアすれば、Hackの基本はバッチリマスターできますよ!JITのパフォーマンスを最大限に引き出す書き方のコツです。
1. 型をはっきり書く(厳格モードの徹底)
`mixed` 型を避け、明確に `int`, `string`, `vec
2. ループ内で不変な「重いオブジェクト生成」はあらかじめ外に出す
JITを過信せず、あきらかに不変なコレクション(`dict` や `vec` など)はループの手前で定義しましょう。
3. 純粋関数(Pure Function)を意識する
入力が同じなら常に同じ結果を返し、外部の状態を変えない関数を書く習慣をつけると、JITは「この関数呼び出しはループの外に出せるぞ!」と判断しやすくなります。
—
まとめ
今回の学びを振り返ってみましょう!
- LICM(ループ不変量コード移動)とは、ループ内の変わらない計算をJITが自動で外に移動させて高速化する仕組み。
- Hackの厳格な静的型システムがあるからこそ、HHVMは安全にコードをループ外へ持ち出すことができる。
- 副作用のある処理や型が不透明なコードはLICMの対象外になりやすいので、きれいな型定義と純粋なコードを心がけるのがコツ!
HHVMのJITコンパイラは、あなたが書いた「正しく美しいHackコード」を極限まで速くするための相棒です。
静的型システムの力を借りて、安全で爆速なアプリケーションをバシバシ作っていきましょうね!応援しています!