FABRKNT
Inside Revm — EVM エンジンを読む
Revmの心臓部
レッスン 3 / 17·CONTENT8 分25 XP
コース
Inside Revm — EVM エンジンを読む
レッスンの役割
CONTENT
順序
3 / 17

レッスン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() >= Npopn_topSome を返すことの関係は ドメイン不変条件、型システムが見える型不変条件ではない、コンパイラは証明できない、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 証明済み。ガード消すと即 UBunsafe = 「自分でチェックした、ランタイムは二重チェック不要」契約。

合格基準

  • 本物の 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 は固定ガスで本体課金なし。