【入門編】HHVMのJITにおけるデッドコード除去の限界:静的解析で消せないコードをどう減らすか – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

やあ、Hackの世界へようこそ!
HHVMのコア開発に長年携わっている先輩エンジニアの私から、今日は皆さんに「HHVMのJITコンパイルとデッドコード除去(DCE: Dead Code Elimination)」という、少しマニアックで、けれど知っておくと一気にコーディングの視点が変わる面白いテーマをお届けします。

「デッドコード除去なんて、コンパイラが勝手にやってくれるんじゃないの?」

そう思ったあなた、半分正解で、半分は惜しい!
HHVMのJITコンパイルは非常に優秀ですが、「人間(開発者)の書き方次第で、JITが消したくても消せない無駄コード」が残ってしまう限界が存在します。

この記事を読み終わる頃には、HHVMが内部でどうコードを間引いているのかが目に見えるようになり、どう書けば最も速く美しいコードになるのかが理解できるようになりますよ。

ここをクリアすれば、Hackの基本はバッチリマスターできます! 一緒に楽しく学んでいきましょう!

—

1. そもそも「デッドコード除去(DCE)」ってなに?

プログラムの中に「絶対に実行されない処理」や「計算しても結果が誰にも使われない処理」があるとき、それをコンパイル時に削り捨てる最適化技術をデッドコード除去(DCE)と呼びます。

HHVMの内部では、Hackコードが以下のようなプロセスを経て超高速な機械語に変換されます。

【Hackコード】 ──> 【HHBC (バイトコード)】 ──> 【HHIR (中間表現)】 ──> 【ネイティブ機械語】
↑
★ここで強力なDCEが走る!

たとえば、こんなコードがあったとしましょう。

<<__EntryPoint>>
function main(): void {
$unused_val = 10 + 20; // 計算結果はどこにも使われていない
echo “Hello, HHVM!\n”;
}

HHVMのJITコンパイラは、HHIR(HipHop Intermediate Representation)という中間表現を解析するときに、「あ、$unused_valの計算結果はどこにも出力されないし、副作用(Side Effect)もないな」と判断します。

すると、JITは機械語を生成する段階で、この加算処理を最初から存在しなかったことにして消去します。これがDCEです。

【最適化イメージ】
[ HHIR段階 ]
t1:int = 10 + 20
t2:string = “Hello, HHVM!\n”
Print t2
↓ DCE(デッドコード除去)実行!
[ 機械語出力 ]
“Hello, HHVM!\n” を画面に出力する命令だけを生成!(加算処理は消滅)

これだけ見ると「なんだ、JITって完璧じゃない!」と思えますよね。しかし、ここには大きな落とし穴(限界)があるのです。

—

2. JITの手を止める「2つの壁」:なぜ消せないコードが生まれるのか?

HHVMのJITコンパイラが「このコード、使われてないから消したいな…」と思っても、絶対に消せなくなるパターンが2つあります。

壁①:副作用(Side Effect)の恐怖

コンパイラは「この処理を消すことで、プログラムの挙動が変わってしまうかもしれない」と1%でも懸念した場合、安全のためにコードを残します。これが「副作用」です。

  • グローバル変数やプロパティの書き換え
  • I/O処理(画面出力、ログ書き込み、DBアクセス)
  • 例外(Exception)を投げる可能性がある処理

壁②:曖昧な「型」とJITガード(Guard)

Hackは強力な静的型付け言語ですが、`mixed` 型を使ったり、抽象クラスのメソッドを動的に呼び出すようなコードを書くと、JITは「実行されるまで本当の型がわからない」状態になります。

型が確定しないと、JITは「型チェック(Guard命令)」と「型が違った場合のフォールバック処理」を機械語の中に残さざるを得なくなり、結果としてデッドコード除去が不発に終わります。

—

3. 実践!「JITフレンドリー」なコードへリファクタリング

具体的なHackのコードを見ながら、JITがコードを消せる例と消せない例を体験してみましょう。

失敗例:JITが消せない「隠れた副作用と型不確かさ」

以下のコードを見てください。一見すると、`unused_process()` の結果はどこにも使われていないため、丸ごと消えてほしいところです。

logStatus(“test”);

echo “Done!\n”;
}

この場合、`$result` という変数の代入自体は無意味ですが、`logStatus` の中の `echo` を実行しなければならないため、JITは関数呼び出し命令を消せません。

成功例:JITが喜ぶ「純粋関数」と「final/厳格な型」

では、これをどう修正すればJITが限界を超えてコードを消去(最適化)できるようになるでしょうか?

1. 副作用を分離する(純粋な計算にする)
2. クラスやメソッドに `final` をつけ、型を明確にする(インライン化を促す)

calculateScore(10);

// ──> JITコンパイル時、calculateScoreの呼び出し自体が『消滅』します!

echo “Done!\n”;
}

なぜこれでコードが消えるの?

HHVMのJITは、`final` がついたクラスのメソッドを呼び出す際、インライン展開(関数呼び出しをその場に埋め込むこと)を行います。

インライン展開された結果、コードは `$score = 10 2 + 100;` と等価になり、さらに `$score` が使われていないことが一目瞭然になるため、最終的に機械語から計算処理がまるごと消去(DCE)されます!

—

4. 初学者が陥りやすい!文法エラーと罠パターン

Hackを書き始めた人が「JIT最適化」を意識する中でハマりがちなポイントをまとめました。

罠1:`mixed` 型を多用して型チェッカー(hh_client)に怒られる

JITを助ける第一歩は、Hackの型チェッカー(`hh_client`)を100%パスさせることです。

// ✕ ダメな例:mixedはJITのGuardを増やし、DCEを阻害する
function process(mixed $val): void { … }

// ◯ 良い例:具体的な型を指定するか、ジェネリクスを使う
function process(int $val): void { … }

罠2:例外を投げる可能性を忘れている

一見純粋な計算に見えても、「ゼロ除算」や「配列の範囲外アクセス」があるコードは、JITからすると「例外を投げるという副作用がある」とみなされ、DCEで消せなくなります。

function compute(int $a, int $b): int {
// $b が 0 の場合、DivisionByZeroException が飛ぶ!
// JITは「例外発生」という挙動を守るため、この処理を完全に消すことができない
return $a / $b;
}

これを避けるには、事前バリデーションを行うか、安全な演算構造にすることが推奨されます。

—

5. まとめ:JITフレンドリーなコードを書くための秘訣

お疲れ様でした! 最後に、HHVMのJITと良好な関係を築くための「黄金ルール」をおさらいしましょう。

1. 型は可能な限り「具体的」に書く (`mixed` を避け、`int` や `string` などの具象型を指定)
2. 継承されないクラス・メソッドには `final` をつける (JITがインライン展開しやすくなり、DCEが爆発的に効く)
3. 計算処理と副作用(I/Oやログ出力)をしっかり分ける (純粋な関数はJITの大好物)

コンパイラ(JIT)の気持ちになって「このコード、余計な動きをしていないかな?」と意識して書くだけで、あなたの書くHackコードは安全性と超高速性能を兼ね備えた極上のコードに生まれ変わります。

これを理解できたあなたなら、Hack言語マスターへの第一歩はすでに踏み出せていますよ!
ぜひ実際のコードでも `final` や厳格な型付けを試してみてくださいね!応援しています!

タイトルとURLをコピーしました