レッスン2 — add を読む:マクロを抽出する
問い
crates/interpreter/src/instructions/arithmetic.rs を開くと add / mul / sub / div / lt / gt / eq 等の二項 Opcode が 30 個以上、全部同じ 2 行のスタック pop ボイラープレートで始まる。revm は popn_top! という 1 マクロで全箇所を置き換える。なぜ関数ではなくマクロか、3 ディテールの価値は?
原理(最小モデル)
- 本物のリファクタ. 前レッスンの
add中央 2 行をpopn_top!([op1], op2, context.interpreter);に置換。 - マクロ選択の 2 理由. ① 可変アリティ(Opcode により pop 数 1/2/3)、② Opcode 関数から直接
return Err(StackUnderflow);発行(?不要)。 - 3 ディテール.
cold_path()(LLVM ヒント)+ アンダーフロー 1 回事前チェック +unwrap_unchecked()(ガード前提で実行時チェックスキップ)。 gas!も同形. チェック → cold ヒント → 早期リターン、5 行版。unsafeの契約.unwrap_unchecked= ガードが証明したドメイン不変条件、ガード消すと即 UB。addは固定ガス本体課金なし. ディスパッチが前払い、exp/sha3等オペランド依存のみ本体課金。
具体例
ステップ 1 — マクロ化対象の 2 行:
let op1 = ctx.interpreter.stack.pop().ok_or(StackUnderflow)?;
let op2 = ctx.interpreter.stack.last_mut().ok_or(StackUnderflow)?;
コードベース全体で 30 回以上繰り返される。問題は リファクタするかどうか ではなく どうやるか。
ステップ 2 — 素朴マクロ:
macro_rules! popn_top_naive {
([ $($x:ident),* ], $top:ident, $interpreter:expr) => {
$(
let $x = $interpreter.stack.pop().ok_or(StackUnderflow)?;
)*
let $top = $interpreter.stack.last_mut().ok_or(StackUnderflow)?;
};
}
$($x:ident),* = カンマ区切り識別子リスト、$( ... )* = リスト要素ごとに繰り返し。これは動く、ただし遅い 2 点で。
ステップ 3 — アンダーフロー 1 回事前チェック:
if $interpreter.stack.len() < (1 + $crate::_count!($($x)*)) {
return Err(StackUnderflow);
}
// ... 以降は再チェックなしで pop
.pop() を N 回呼ぶ = 内部境界チェック N 回。最初に 1 回だけチェック → 以降の pop は静的に安全。
ステップ 4 — cold_path() で LLVM ヒント:
if $interpreter.stack.len() < (1 + $crate::_count!($($x)*)) {
$crate::primitives::hints_util::cold_path();
return Err(StackUnderflow);
}
cold_path() は 実行時には何にもコンパイルされない、LLVM への「この分岐から到達するコードはレア」というヒント。レア分岐コードをホット icache から離れた場所へ配置、ハッピーパス 1 本直線アセンブリ。ゼロコスト最適化ヒント。
ステップ 5 — unwrap_unchecked() でガード恩恵回収:
let ([$( $x ),*], $top) = unsafe {
$crate::interpreter_types::StackTr::popn_top(&mut $interpreter.stack)
.unwrap_unchecked()
};
unwrap_unchecked() は実行時の Some チェックをスキップ。安全なのは値が Some であることを証明できるときだけ — ステップ 3 のガードがそれを証明済み。unsafe ブロックは契約: 「自分でチェックした、二重チェック不要」。
ステップ 6 — 本物:
macro_rules! popn_top {
([ $($x:ident),* ], $top:ident, $interpreter:expr) => {
if $interpreter.stack.len() < (1 + $crate::_count!($($x)*)) {
$crate::primitives::hints_util::cold_path();
return Err($crate::InstructionResult::StackUnderflow);
}
let ([$( $x ),*], $top) = unsafe {
$crate::interpreter_types::StackTr::popn_top(&mut $interpreter.stack)
.unwrap_unchecked()
};
};
}
3 ディテール:
cold_path()— LLVM へのコンパイル時ヒント「この分岐から到達するコードはレア」、レア分岐コードをホット icache から離れた場所へ配置、ハッピーパス 1 本直線アセンブリ- アンダーフロー 1 回事前チェック — N 回
.pop()呼ぶ = 境界チェック N 回 → 最初に 1 回stack.len() < (1 + N)で済ます unwrap_unchecked()— ガードがSomeを証明済み、実行時Someチェックをスキップ
gas!:
macro_rules! gas {
($interpreter:expr, $gas:expr) => {
if !$interpreter.gas.record_regular_cost($gas) {
$crate::primitives::hints_util::cold_path();
return Err($crate::InstructionResult::OutOfGas);
}
};
}
チェック → cold ヒント → 早期リターン、5 行版。
失敗例(誤解)
「マクロでなく関数で十分」— 間違い。① 可変アリティ(popn_top1/2/3 別関数 or const ジェネリクス曲芸)、② 関数だと呼び出し側で毎回 ? のお決まり構文。
「cold_path() は panic ハンドラへのジャンプ」— 間違い。実行時には何にもコンパイルされない、LLVM へのヒントのみ。
「コンパイラが冗長な Some チェックを最適化で除去できる」— 間違い。stack.len() >= N と popn_top が Some を返すことの関係は ドメイン不変条件、型システムが見える型不変条件ではない、コンパイラは証明できない、unwrap_unchecked がドメイン知識と型システムの継ぎ目。
ステップで組み立てる
Step 1: マクロを選ぶ 2 理由
可変アリティ + 直接早期リターン。
Step 2: 素朴マクロ
$($x:ident),* 繰り返しで各 pop。
Step 3: アンダーフロー 1 回事前チェック
stack.len() < (1 + N) で以降の pop は静的に安全。
Step 4: cold_path() で LLVM ヒント
レア分岐コードをホット icache から離す、実行時コストゼロ。
Step 5: unwrap_unchecked() でガード恩恵回収
ガードが Some 証明済み、実行時チェックスキップ、unsafe 契約。
Step 6: gas! も同形
チェック → cold ヒント → 早期リターン、5 行版。
答え合わせ
- マクロ選択の機構的理由: ① 可変アリティ =
[op1]/[op1, op2]/[op1, op2, op3]を同じアームでマッチ、関数だと N 別関数、② 直接早期リターン =return Errを Opcode 関数から発行、関数だとResult+?。 cold_path()の実行時挙動: 何にもコンパイルされない、LLVM への「この分岐は統計的にレア」ヒント。レア分岐コードをホットパスから離れた場所に配置、ハッピーパスはキャッシュ温度維持の 1 本直線アセンブリ。unwrap_uncheckedが UB にならない理由: マクロ直前のif stack.len() < ...ガードが pop する値がSomeであることを 静的に保証、unwrap_unchecked実行時にはSome証明済み。ガード消すと即 UB、unsafe= 「自分でチェックした、ランタイムは二重チェック不要」契約。
合格基準
- 本物の
popn_top!6 ステップ展開を即書ける。 - マクロ選択の 2 理由を 1 文ずつ説明できる。
cold_path()の実行時挙動(何も生成しない LLVM ヒント)を即答できる。unwrap_uncheckedがガード前提で UB にならない理由を 1 文で説明できる。add固定ガス本体課金なし、exp/sha3本体課金(オペランド依存)の差を言える。
まとめ(3行)
- 本物の
popn_top!= アンダーフロー 1 回事前チェック +cold_path()LLVM ヒント +unwrap_unchecked()ガード恩恵回収 + 可変アリティ識別子マッチ。 - マクロ選択 2 理由 = 可変アリティ + 直接早期リターン、関数だと N 別関数 +
?ボイラープレート。 gas!も同形(チェック → cold ヒント → 早期リターン)=popn_top!を消化すれば 5 行で読める、addは固定ガスで本体課金なし。