diff --git a/third_party/move/mono-move/core/src/function.rs b/third_party/move/mono-move/core/src/function.rs index 31fbd03b1b2..a35437ba25b 100644 --- a/third_party/move/mono-move/core/src/function.rs +++ b/third_party/move/mono-move/core/src/function.rs @@ -183,6 +183,11 @@ pub struct Function { /// frames may scan the same memory. The forwarding markers in /// `gc_copy_object` handle double-scans correctly. pub frame_layout: FrameLayoutInfo, + /// Static gas cost of the entry basic block (block 0). + /// + /// Charged at call time, before any instruction in block 0 executes + /// (charge-before-work). Set by [`mono_move_gas::GasInstrumentor::run`]. + pub entry_gas: u64, /// Per-safe-point frame layouts. /// /// During GC, for each frame on the call stack, the GC scans the @@ -222,6 +227,17 @@ impl Function { unsafe { self.safe_point_layouts.layout_at(pc) } } + /// Replaces every [`MicroOp::CallFunc`] (index-based dispatch) with + /// [`MicroOp::CallLocalFunc`] (direct pointer dispatch). + /// + /// `func_ptrs` is indexed by definition index and may contain `None` + /// for functions that were not lowered (e.g. generic functions). + /// + /// # Safety + /// + /// The caller must have exclusive access to the functions and their + /// arena-allocated code. The arena must outlive all uses of the patched + /// code. /// Replaces every [`MicroOp::CallFunc`] (index-based dispatch) with /// [`MicroOp::CallLocalFunc`] (direct pointer dispatch). /// diff --git a/third_party/move/mono-move/core/src/instruction/gas.rs b/third_party/move/mono-move/core/src/instruction/gas.rs index c6bacc566f3..8f5fa70dbc1 100644 --- a/third_party/move/mono-move/core/src/instruction/gas.rs +++ b/third_party/move/mono-move/core/src/instruction/gas.rs @@ -8,17 +8,7 @@ //! framework. Plug in a different ISA by writing an equivalent file. use super::{CodeOffset, MicroOp}; -use mono_move_gas::{GasMeteredInstruction, GasSchedule, HasCfgInfo, InstrCost, RemapTargets}; - -// --------------------------------------------------------------------------- -// GasMeteredInstruction -// --------------------------------------------------------------------------- - -impl GasMeteredInstruction for MicroOp { - fn charge(cost: u64) -> Self { - MicroOp::Charge { cost } - } -} +use mono_move_gas::{ChargeOnJump, GasSchedule, HasCfgInfo}; // --------------------------------------------------------------------------- // HasCfgInfo @@ -27,7 +17,7 @@ impl GasMeteredInstruction for MicroOp { impl HasCfgInfo for MicroOp { fn branch_target(&self) -> Option { match self { - MicroOp::Jump { target } + MicroOp::Jump { target, .. } | MicroOp::JumpNotZeroU64 { target, .. } | MicroOp::JumpGreaterEqualU64Imm { target, .. } | MicroOp::JumpLessU64Imm { target, .. } @@ -64,7 +54,6 @@ impl HasCfgInfo for MicroOp { | MicroOp::HeapMoveTo8 { .. } | MicroOp::HeapMoveToImm8 { .. } | MicroOp::HeapMoveTo { .. } - | MicroOp::Charge { .. } | MicroOp::StoreRandomU64 { .. } | MicroOp::ForceGC => None, } @@ -72,78 +61,74 @@ impl HasCfgInfo for MicroOp { } // --------------------------------------------------------------------------- -// RemapTargets +// ChargeOnJump // --------------------------------------------------------------------------- -impl RemapTargets for MicroOp { - fn remap_targets(self, remap: impl Fn(usize) -> usize) -> Self { - let co = |c: CodeOffset| CodeOffset(remap(c.0 as usize) as u32); +impl ChargeOnJump for MicroOp { + fn with_gas(self, taken: u64, fallthrough: u64) -> Self { + // Saturate to u32 for the conditional-jump gas fields. + // Block costs fit comfortably in u32 for any realistic program. + let gt = taken.min(u32::MAX as u64) as u32; + let gf = fallthrough.min(u32::MAX as u64) as u32; + let co = |c: CodeOffset| c; // no-op, just for symmetry match self { - MicroOp::Jump { target } => MicroOp::Jump { target: co(target) }, - MicroOp::JumpNotZeroU64 { target, src } => MicroOp::JumpNotZeroU64 { + MicroOp::Jump { target, .. } => MicroOp::Jump { target: co(target), - src, + gas: taken, }, - MicroOp::JumpGreaterEqualU64Imm { target, src, imm } => { - MicroOp::JumpGreaterEqualU64Imm { - target: co(target), - src, - imm, - } + MicroOp::JumpNotZeroU64 { target, src, .. } => MicroOp::JumpNotZeroU64 { + target, + src, + gas_taken: gt, + gas_fallthrough: gf, }, - MicroOp::JumpLessU64 { target, lhs, rhs } => MicroOp::JumpLessU64 { - target: co(target), - lhs, - rhs, + MicroOp::JumpGreaterEqualU64Imm { + target, src, imm, .. + } => MicroOp::JumpGreaterEqualU64Imm { + target, + src, + imm, + gas_taken: gt, + gas_fallthrough: gf, }, - MicroOp::JumpLessU64Imm { target, src, imm } => MicroOp::JumpLessU64Imm { - target: co(target), + MicroOp::JumpLessU64Imm { + target, src, imm, .. + } => MicroOp::JumpLessU64Imm { + target, src, imm, + gas_taken: gt, + gas_fallthrough: gf, }, - MicroOp::JumpGreaterEqualU64 { target, lhs, rhs } => MicroOp::JumpGreaterEqualU64 { - target: co(target), + MicroOp::JumpLessU64 { + target, lhs, rhs, .. + } => MicroOp::JumpLessU64 { + target, lhs, rhs, + gas_taken: gt, + gas_fallthrough: gf, }, - MicroOp::JumpNotEqualU64 { target, lhs, rhs } => MicroOp::JumpNotEqualU64 { - target: co(target), + MicroOp::JumpGreaterEqualU64 { + target, lhs, rhs, .. + } => MicroOp::JumpGreaterEqualU64 { + target, lhs, rhs, + gas_taken: gt, + gas_fallthrough: gf, }, - op @ (MicroOp::StoreImm8 { .. } - | MicroOp::Move8 { .. } - | MicroOp::Move { .. } - | MicroOp::AddU64 { .. } - | MicroOp::AddU64Imm { .. } - | MicroOp::SubU64Imm { .. } - | MicroOp::RSubU64Imm { .. } - | MicroOp::XorU64 { .. } - | MicroOp::ShrU64Imm { .. } - | MicroOp::ModU64 { .. } - | MicroOp::Return - | MicroOp::CallFunc { .. } - | MicroOp::CallLocalFunc { .. } - | MicroOp::VecNew { .. } - | MicroOp::VecLen { .. } - | MicroOp::VecPushBack { .. } - | MicroOp::VecPopBack { .. } - | MicroOp::VecLoadElem { .. } - | MicroOp::VecStoreElem { .. } - | MicroOp::SlotBorrow { .. } - | MicroOp::VecBorrow { .. } - | MicroOp::HeapBorrow { .. } - | MicroOp::ReadRef { .. } - | MicroOp::WriteRef { .. } - | MicroOp::HeapNew { .. } - | MicroOp::HeapMoveFrom8 { .. } - | MicroOp::HeapMoveFrom { .. } - | MicroOp::HeapMoveTo8 { .. } - | MicroOp::HeapMoveToImm8 { .. } - | MicroOp::HeapMoveTo { .. } - | MicroOp::Charge { .. } - | MicroOp::StoreRandomU64 { .. } - | MicroOp::ForceGC) => op, + MicroOp::JumpNotEqualU64 { + target, lhs, rhs, .. + } => MicroOp::JumpNotEqualU64 { + target, + lhs, + rhs, + gas_taken: gt, + gas_fallthrough: gf, + }, + // Non-jump instructions: no gas parameter to embed. + other => other, } } } @@ -158,8 +143,8 @@ impl RemapTargets for MicroOp { pub struct MicroOpGasSchedule; impl GasSchedule for MicroOpGasSchedule { - fn cost(&self, instr: &MicroOp) -> InstrCost { - InstrCost::constant(match instr { + fn cost(&self, instr: &MicroOp) -> u64 { + match instr { // --- Data movement --- MicroOp::StoreImm8 { .. } => 2, MicroOp::Move8 { .. } => 2, @@ -208,12 +193,9 @@ impl GasSchedule for MicroOpGasSchedule { 2 + 3 * *size as u64 }, - // --- Gas metering (inserted by instrumentation; not in input) --- - MicroOp::Charge { .. } => 0, - // --- Debug --- MicroOp::StoreRandomU64 { .. } => 1, MicroOp::ForceGC => 100, - }) + } } } diff --git a/third_party/move/mono-move/core/src/instruction/mod.rs b/third_party/move/mono-move/core/src/instruction/mod.rs index 8fa7ba6dfdf..593fafcfe3f 100644 --- a/third_party/move/mono-move/core/src/instruction/mod.rs +++ b/third_party/move/mono-move/core/src/instruction/mod.rs @@ -176,7 +176,7 @@ impl std::fmt::Display for DescriptorId { } } -#[derive(Debug)] +#[derive(Copy, Clone, Debug)] pub enum MicroOp { //====================================================================== // Data movement @@ -295,12 +295,21 @@ pub enum MicroOp { Return, /// Unconditional jump. - Jump { target: CodeOffset }, + /// + /// `gas` is the static cost of the destination block, charged before + /// the jump executes (charge-before-work). + Jump { target: CodeOffset, gas: u64 }, /// Jump to `target` if the u64 at `src` is **not** zero. + /// + /// `gas_taken` / `gas_fallthrough` are the static costs of the taken and + /// fallthrough destination blocks, respectively. The interpreter charges + /// exactly one of these before transferring control. JumpNotZeroU64 { target: CodeOffset, src: FrameOffset, + gas_taken: u32, + gas_fallthrough: u32, }, /// Jump to `target` if the u64 at `src` is **>=** `imm`. @@ -308,6 +317,8 @@ pub enum MicroOp { target: CodeOffset, src: FrameOffset, imm: u64, + gas_taken: u32, + gas_fallthrough: u32, }, /// Jump to `target` if the u64 at `src` is **<** `imm`. @@ -315,6 +326,8 @@ pub enum MicroOp { target: CodeOffset, src: FrameOffset, imm: u64, + gas_taken: u32, + gas_fallthrough: u32, }, /// Jump to `target` if u64 at `lhs` < u64 at `rhs`. @@ -322,6 +335,8 @@ pub enum MicroOp { target: CodeOffset, lhs: FrameOffset, rhs: FrameOffset, + gas_taken: u32, + gas_fallthrough: u32, }, /// Jump to `target` if u64 at `lhs` >= u64 at `rhs`. @@ -329,6 +344,8 @@ pub enum MicroOp { target: CodeOffset, lhs: FrameOffset, rhs: FrameOffset, + gas_taken: u32, + gas_fallthrough: u32, }, /// Jump to `target` if u64 at `lhs` != u64 at `rhs`. @@ -336,6 +353,8 @@ pub enum MicroOp { target: CodeOffset, lhs: FrameOffset, rhs: FrameOffset, + gas_taken: u32, + gas_fallthrough: u32, }, //====================================================================== @@ -546,15 +565,6 @@ pub enum MicroOp { size: u32, }, - //====================================================================== - // Gas metering - //====================================================================== - // Inserted by the instrumentation pass; never emitted directly by user code. - //====================================================================== - /// Charge a pre-computed static gas cost for the current basic block. - /// The interpreter must call the gas meter and abort on exhaustion. - Charge { cost: u64 }, - //====================================================================== // Debugging //====================================================================== @@ -620,21 +630,46 @@ impl fmt::Display for MicroOp { MicroOp::Return => { write!(f, "Return") }, - MicroOp::Jump { target } => { - write!(f, "Jump @{}", target.0) + MicroOp::Jump { target, gas } => { + write!(f, "Jump @{} gas={}", target.0, gas) }, - MicroOp::JumpNotZeroU64 { target, src } => { - write!(f, "JumpNotZeroU64 @{} [{}]", target.0, src.0) + MicroOp::JumpNotZeroU64 { + target, + src, + gas_taken, + gas_fallthrough, + } => { + write!( + f, + "JumpNotZeroU64 @{} [{}] gas={}/{}", + target.0, src.0, gas_taken, gas_fallthrough + ) }, - MicroOp::JumpGreaterEqualU64Imm { target, src, imm } => { + MicroOp::JumpGreaterEqualU64Imm { + target, + src, + imm, + gas_taken, + gas_fallthrough, + } => { write!( f, - "JumpGreaterEqualU64Imm @{} [{}] >= #{}", - target.0, src.0, imm + "JumpGreaterEqualU64Imm @{} [{}] >= #{} gas={}/{}", + target.0, src.0, imm, gas_taken, gas_fallthrough ) }, - MicroOp::JumpLessU64 { target, lhs, rhs } => { - write!(f, "JumpLessU64 @{} [{}] < [{}]", target.0, lhs.0, rhs.0) + MicroOp::JumpLessU64 { + target, + lhs, + rhs, + gas_taken, + gas_fallthrough, + } => { + write!( + f, + "JumpLessU64 @{} [{}] < [{}] gas={}/{}", + target.0, lhs.0, rhs.0, gas_taken, gas_fallthrough + ) }, MicroOp::VecNew { dst } => { write!(f, "VecNew [{}]", dst.0) @@ -779,29 +814,48 @@ impl fmt::Display for MicroOp { MicroOp::XorU64 { dst, lhs, rhs } => { write!(f, "XorU64 [{}] <- [{}] ^ [{}]", dst.0, lhs.0, rhs.0) }, - MicroOp::JumpLessU64Imm { target, src, imm } => { - write!(f, "JumpLessU64Imm @{} [{}] < #{}", target.0, src.0, imm) + MicroOp::JumpLessU64Imm { + target, + src, + imm, + gas_taken, + gas_fallthrough, + } => { + write!( + f, + "JumpLessU64Imm @{} [{}] < #{} gas={}/{}", + target.0, src.0, imm, gas_taken, gas_fallthrough + ) }, - MicroOp::JumpGreaterEqualU64 { target, lhs, rhs } => { + MicroOp::JumpGreaterEqualU64 { + target, + lhs, + rhs, + gas_taken, + gas_fallthrough, + } => { write!( f, - "JumpGreaterEqualU64 @{} [{}] >= [{}]", - target.0, lhs.0, rhs.0 + "JumpGreaterEqualU64 @{} [{}] >= [{}] gas={}/{}", + target.0, lhs.0, rhs.0, gas_taken, gas_fallthrough ) }, - MicroOp::JumpNotEqualU64 { target, lhs, rhs } => { + MicroOp::JumpNotEqualU64 { + target, + lhs, + rhs, + gas_taken, + gas_fallthrough, + } => { write!( f, - "JumpNotEqualU64 @{} [{}] != [{}]", - target.0, lhs.0, rhs.0 + "JumpNotEqualU64 @{} [{}] != [{}] gas={}/{}", + target.0, lhs.0, rhs.0, gas_taken, gas_fallthrough ) }, MicroOp::ForceGC => { write!(f, "ForceGC") }, - MicroOp::Charge { cost } => { - write!(f, "Charge #{}", cost) - }, } } } @@ -957,8 +1011,8 @@ mod tests { #[test] fn micro_op_size() { - // Current size is 24 bytes due to large variants (e.g. + // Current size is 32 bytes due to large variants (e.g. // JumpGreaterEqualU64Imm). We should aim to bring this down to 16. - assert_eq!(std::mem::size_of::(), 24); + assert_eq!(std::mem::size_of::(), 32); } } diff --git a/third_party/move/mono-move/gas/src/instrument.rs b/third_party/move/mono-move/gas/src/instrument.rs index bdbd3e7bdd8..75d4526fe61 100644 --- a/third_party/move/mono-move/gas/src/instrument.rs +++ b/third_party/move/mono-move/gas/src/instrument.rs @@ -1,70 +1,75 @@ // Copyright (c) Aptos Foundation // Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE -//! Generic instrumentation pass: insert gas charge ops into a flat -//! instruction sequence. +//! Generic instrumentation pass: embed block gas costs into jump instructions. //! -//! Two kinds of charge ops are inserted: +//! Each basic block's static cost is baked into the jump instructions that +//! *enter* the block, so the interpreter charges gas before executing any +//! work in the block (charge-before-work). //! -//! - **`Charge`** — for each basic block, prepended at the block entry -//! with the summed static costs of all instructions in the block. -//! - An optional dynamic charge op — for each instruction with a -//! runtime-variable cost component, appended immediately after it. +//! ## Gas embedding //! -//! ## Dead code +//! - **Entry block (block 0)**: its cost is returned as `entry_gas` from +//! [`GasInstrumentor::run`] and stored in the function's metadata. The +//! runtime charges it at call time, before any instruction in block 0 +//! executes. //! -//! The pass instruments every basic block, including unreachable ones, -//! potentially doubling program size in the worst case. +//! - **All other blocks**: each block's cost is written into the jump +//! instruction(s) that target the block. The interpreter charges the cost +//! before taking the jump. //! -//! TODO: the compiler should eliminate dead basic blocks before this pass -//! runs, both to avoid wasted allocation and to prevent dead `Charge` ops -//! from polluting the instruction cache. +//! For **unconditional** jumps, `with_gas` receives the destination block's +//! cost as `taken` and `0` as `fallthrough`. //! -//! ## Branch-target remapping +//! For **conditional** jumps, `with_gas` receives the taken block's cost as +//! `taken` and the fallthrough block's cost as `fallthrough`. //! -//! Inserting charge ops shifts instruction indices, so all branch targets are -//! rewritten to account for all inserted charge ops. The [`RemapTargets`] -//! trait lets each instruction type perform this rewrite without the gas -//! crate knowing instruction internals. +//! For **return** and other instructions without a branch target, `with_gas` +//! is not called — these terminators carry no gas charge because their block's +//! cost was already charged on entry. +//! +//! ## No instruction insertion +//! +//! This pass produces an output sequence of the **same length** as the input. +//! Branch targets remain valid without any remapping. -use crate::{compute_basic_blocks, GasSchedule, HasCfgInfo, InstrCost}; +use crate::{compute_basic_blocks, GasSchedule, HasCfgInfo}; // --------------------------------------------------------------------------- // Traits // --------------------------------------------------------------------------- -/// Constructs gas charge instructions within an ISA. +/// Embeds destination-block gas costs into a jump instruction. /// -/// Implemented alongside the ISA's instruction type. -pub trait GasMeteredInstruction: Sized { - /// Construct a static gas charge. - /// - /// In practice, `cost` will be the pre-summed static cost of every - /// instruction in a basic block, computed at instrumentation time. - fn charge(cost: u64) -> Self; -} - -/// Rewrites branch-target instruction indices inside an instruction. +/// Implemented alongside the ISA's instruction type. [`GasInstrumentor::run`] +/// calls `with_gas` on every jump instruction (those for which +/// [`HasCfgInfo::branch_target`] returns `Some`). +/// +/// - `taken`: cost of the block reached by the primary jump target. +/// - `fallthrough`: cost of the block reached by falling through to the next +/// instruction. Zero for unconditional jumps (their fallthrough is dead code +/// and is never charged). /// -/// Implemented alongside [`HasCfgInfo`]. [`GasInstrumentor::run`] calls -/// this on every instruction to fix up branch targets after inserting charge -/// ops. Non-branching instructions return `self` unchanged. -pub trait RemapTargets: Sized + HasCfgInfo { - /// Return a copy of `self` with every branch target index `t` replaced - /// by `remap(t)`. - fn remap_targets(self, remap: impl Fn(usize) -> usize) -> Self; +/// For non-jump instructions (return, arithmetic, etc.) the implementation +/// should return `self` unchanged. +pub trait ChargeOnJump: Sized { + /// Return a copy of `self` with the given block costs embedded. + fn with_gas(self, taken: u64, fallthrough: u64) -> Self; } // --------------------------------------------------------------------------- // GasInstrumentor // --------------------------------------------------------------------------- -/// Inserts gas charge ops into a flat instruction sequence. +/// Annotates jump instructions with destination-block gas costs. /// -/// For each basic block, prepends a static block charge with the summed base -/// costs. For each `Dynamic`-cost instruction, also inserts a runtime charge -/// immediately after it. Branch targets are remapped to account for all -/// inserted ops. +/// For each basic block, computes the sum of the base costs of all +/// instructions, then writes that cost into every jump instruction that +/// targets the block. The entry block's cost is returned separately as +/// `entry_gas` and must be charged at the call site. +/// +/// The output sequence has the **same length** as the input — no instructions +/// are inserted and no branch targets need remapping. /// /// TODO: this runs as a separate pass over the instruction sequence. Ideally /// it would be fused with an earlier compiler pass to avoid redundant @@ -79,54 +84,78 @@ impl GasInstrumentor { Self { schedule } } - pub fn run(&self, ops: Vec) -> Vec + /// Instrument `ops` and return `(annotated_ops, entry_gas)`. + /// + /// `entry_gas` is the static cost of block 0. The caller must store it in + /// the function's metadata and charge it at call time before executing any + /// instruction. + pub fn run(&self, ops: Vec) -> (Vec, u64) where - I: HasCfgInfo + RemapTargets + GasMeteredInstruction, + I: HasCfgInfo + ChargeOnJump, S: GasSchedule, { if ops.is_empty() { - return vec![]; + return (vec![], 0); } let blocks = compute_basic_blocks(&ops); - let block_starts: Vec = blocks.iter().map(|bb| bb.start).collect(); - - let costs: Vec> = ops.iter().map(|op| self.schedule.cost(op)).collect(); + // Compute the static cost of each basic block. let block_costs: Vec = blocks .iter() - .map(|bb| (bb.start..bb.end).map(|i| costs[i].base).sum()) - .collect(); - - // d_before[i] = number of dynamic-cost instructions at indices < i. - let mut n_dynamic = 0usize; - let d_before: Vec = costs - .iter() - .map(|c| { - let d = n_dynamic; - if c.dynamic.is_some() { - n_dynamic += 1; - } - d + .map(|bb| { + (bb.start..bb.end) + .map(|i| self.schedule.cost(&ops[i])) + .sum() }) .collect(); - let remap = |t: usize| t + block_starts.partition_point(|&s| s < t) + d_before[t]; + // entry_gas = cost of block 0, charged at function call time. + let entry_gas = block_costs.first().copied().unwrap_or(0); - let mut result = Vec::with_capacity(ops.len() + blocks.len() + n_dynamic); - let mut bi = 0usize; - for (i, (op, cost)) in ops.into_iter().zip(costs).enumerate() { - if bi < block_starts.len() && block_starts[bi] == i { - result.push(I::charge(block_costs[bi])); - bi += 1; - } - result.push(op.remap_targets(remap)); - if let Some(dynamic) = cost.dynamic { - result.push(dynamic); + // Build a lookup table: cost_at_start[i] = cost of the block whose + // leader is at index i, or 0 if no block starts there. + // Size is ops.len()+1 so that `bb.end` for the last block is in bounds. + let mut cost_at_start = vec![0u64; ops.len() + 1]; + for (bb, &cost) in blocks.iter().zip(&block_costs) { + cost_at_start[bb.start] = cost; + } + + // For each block terminator that is a jump, record the (taken, fallthrough) + // destination costs to embed. + let mut gas_vec: Vec> = vec![None; ops.len()]; + for bb in &blocks { + let term_idx = bb.end - 1; + if let Some(taken_target) = ops[term_idx].branch_target() { + let taken_cost = *cost_at_start.get(taken_target).unwrap_or(&0); + let fallthrough_cost = *cost_at_start.get(bb.end).unwrap_or(&0); + gas_vec[term_idx] = Some((taken_cost, fallthrough_cost)); } + // Return and other non-jump terminators: no gas annotation. } - result + let instrumented = ops + .into_iter() + .enumerate() + .map(|(i, op)| match gas_vec[i] { + Some((taken, fallthrough)) => op.with_gas(taken, fallthrough), + None => op, + }) + .collect(); + + (instrumented, entry_gas) + } + + /// Instrument every function in `program` and return the results in the + /// same order as `(annotated_ops, entry_gas)` pairs. + /// + /// Equivalent to calling [`run`](Self::run) on each element individually. + pub fn run_all(&self, program: Vec>) -> Vec<(Vec, u64)> + where + I: HasCfgInfo + ChargeOnJump, + S: GasSchedule, + { + program.into_iter().map(|ops| self.run(ops)).collect() } } @@ -137,262 +166,229 @@ impl GasInstrumentor { #[cfg(test)] mod tests { use super::*; + use crate::GasSchedule; // Minimal stub instruction set for testing. + // + // Jump and CondJump carry gas fields that `with_gas` populates. + // Return and Nop have no gas fields. #[derive(Debug, Clone, PartialEq)] enum TestOp { Nop, - Jump(usize), - CondJump(usize), + Jump { + target: usize, + gas: u64, + }, + CondJump { + target: usize, + gas_taken: u64, + gas_fallthrough: u64, + }, Return, - Charge(u64), - ChargeDynamic(u64), } - impl GasMeteredInstruction for TestOp { - fn charge(cost: u64) -> Self { - TestOp::Charge(cost) + impl ChargeOnJump for TestOp { + fn with_gas(self, taken: u64, fallthrough: u64) -> Self { + match self { + TestOp::Jump { target, .. } => TestOp::Jump { target, gas: taken }, + TestOp::CondJump { target, .. } => TestOp::CondJump { + target, + gas_taken: taken, + gas_fallthrough: fallthrough, + }, + other => other, + } } } impl HasCfgInfo for TestOp { fn branch_target(&self) -> Option { match self { - TestOp::Jump(t) | TestOp::CondJump(t) => Some(*t), + TestOp::Jump { target, .. } | TestOp::CondJump { target, .. } => Some(*target), _ => None, } } } - impl RemapTargets for TestOp { - fn remap_targets(self, remap: impl Fn(usize) -> usize) -> Self { - match self { - TestOp::Jump(t) => TestOp::Jump(remap(t)), - TestOp::CondJump(t) => TestOp::CondJump(remap(t)), - other => other, - } - } - } - #[test] fn empty() { struct TestSchedule; impl GasSchedule for TestSchedule { - fn cost(&self, _: &TestOp) -> InstrCost { - InstrCost::constant(0) + fn cost(&self, _: &TestOp) -> u64 { + 0 } } - let r: Vec = GasInstrumentor::new(TestSchedule).run(vec![]); - assert!(r.is_empty()); + let (ops, entry_gas) = GasInstrumentor::new(TestSchedule).run(vec![]); + assert!(ops.is_empty()); + assert_eq!(entry_gas, 0); } + /// Single block: entry_gas = sum of all instruction costs; no jump to annotate. + /// /// Input: /// 0: Nop — cost 2 /// 1: Nop — cost 2 /// 2: Return — cost 2 /// /// Output: - /// 0: Charge(6) - /// 1: Nop - /// 2: Nop - /// 3: Return + /// ops: [Nop, Nop, Return] (unchanged — Return has no branch_target) + /// entry_gas: 6 #[test] - fn single_block_cost_is_sum() { + fn single_block_entry_gas_is_sum() { struct TestSchedule; impl GasSchedule for TestSchedule { - fn cost(&self, _: &TestOp) -> InstrCost { - InstrCost::constant(2) + fn cost(&self, _: &TestOp) -> u64 { + 2 } } let ops = vec![TestOp::Nop, TestOp::Nop, TestOp::Return]; - let r: Vec = GasInstrumentor::new(TestSchedule).run(ops); - assert_eq!(r, vec![ - TestOp::Charge(6), - TestOp::Nop, - TestOp::Nop, - TestOp::Return, - ]); + let (result, entry_gas) = GasInstrumentor::new(TestSchedule).run(ops); + assert_eq!(result, vec![TestOp::Nop, TestOp::Nop, TestOp::Return]); + assert_eq!(entry_gas, 6); } + /// Unconditional jump carries the destination block's cost. + /// /// Input: /// 0: Jump(2) — cost 1 - /// 1: Nop — cost 1 + /// 1: Nop — cost 1 (dead block — cost never charged) /// 2: Return — cost 1 /// + /// Blocks: + /// [0..1]: Jump(2), cost 1 → entry_gas + /// [1..2]: Nop, cost 1 (dead) + /// [2..3]: Return, cost 1 + /// + /// Jump(2).gas = cost(block at 2) = 1 + /// /// Output: - /// 0: Charge(1) - /// 1: Jump(4) - /// 2: Charge(1) - /// 3: Nop - /// 4: Charge(1) - /// 5: Return + /// ops: [Jump { target:2, gas:1 }, Nop, Return] + /// entry_gas: 1 #[test] - fn jump_target_remapped_to_charge() { + fn unconditional_jump_carries_dest_cost() { struct TestSchedule; impl GasSchedule for TestSchedule { - fn cost(&self, _: &TestOp) -> InstrCost { - InstrCost::constant(1) + fn cost(&self, _: &TestOp) -> u64 { + 1 } } - let ops = vec![TestOp::Jump(2), TestOp::Nop, TestOp::Return]; - let r: Vec = GasInstrumentor::new(TestSchedule).run(ops); - assert_eq!(r, vec![ - TestOp::Charge(1), - TestOp::Jump(4), - TestOp::Charge(1), + let ops = vec![ + TestOp::Jump { target: 2, gas: 0 }, + TestOp::Nop, + TestOp::Return, + ]; + let (result, entry_gas) = GasInstrumentor::new(TestSchedule).run(ops); + assert_eq!(result, vec![ + TestOp::Jump { target: 2, gas: 1 }, TestOp::Nop, - TestOp::Charge(1), TestOp::Return, ]); + assert_eq!(entry_gas, 1); } + /// Conditional jump carries both the taken and fallthrough block costs. + /// Back-edge jump carries the loop header's cost (ensuring correct per-iteration charge). + /// /// Input: - /// 0: CondJump(3) — cost 1 + /// 0: CondJump(3) — cost 1 (loop header: taken → exit, fallthrough → body) /// 1: Nop — cost 1 - /// 2: Jump(0) — cost 1 + /// 2: Jump(0) — cost 1 (back edge: destination is the loop header) /// 3: Return — cost 1 /// + /// Blocks and costs: + /// [0..1]: CondJump, cost 1 → entry_gas = 1 + /// [1..3]: {Nop, Jump(0)}, cost 2 + /// [3..4]: Return, cost 1 + /// + /// CondJump(3): gas_taken = cost(block at 3) = 1, gas_fallthrough = cost(block at 1) = 2 + /// Jump(0): gas = cost(block at 0) = 1 + /// /// Output: - /// 0: Charge(1) - /// 1: CondJump(5) - /// 2: Charge(2) - /// 3: Nop - /// 4: Jump(0) - /// 5: Charge(1) - /// 6: Return + /// ops: [CondJump{3,taken=1,fall=2}, Nop, Jump{0,gas=1}, Return] + /// entry_gas: 1 #[test] - fn back_edge_remapped_to_charge() { + fn back_edge_carries_loop_header_cost() { struct TestSchedule; impl GasSchedule for TestSchedule { - fn cost(&self, _: &TestOp) -> InstrCost { - InstrCost::constant(1) + fn cost(&self, _: &TestOp) -> u64 { + 1 } } let ops = vec![ - TestOp::CondJump(3), + TestOp::CondJump { + target: 3, + gas_taken: 0, + gas_fallthrough: 0, + }, TestOp::Nop, - TestOp::Jump(0), + TestOp::Jump { target: 0, gas: 0 }, TestOp::Return, ]; - let r: Vec = GasInstrumentor::new(TestSchedule).run(ops); - assert_eq!(r, vec![ - TestOp::Charge(1), - TestOp::CondJump(5), - TestOp::Charge(2), + let (result, entry_gas) = GasInstrumentor::new(TestSchedule).run(ops); + assert_eq!(result, vec![ + TestOp::CondJump { + target: 3, + gas_taken: 1, + gas_fallthrough: 2 + }, TestOp::Nop, - TestOp::Jump(0), - TestOp::Charge(1), + TestOp::Jump { target: 0, gas: 1 }, TestOp::Return, ]); + assert_eq!(entry_gas, 1); } + /// Mixed instruction costs: verify that block sums are correct. + /// /// Input: /// 0: Nop — cost 1 /// 1: CondJump(2) — cost 2 /// 2: Nop — cost 1 /// 3: Return — cost 3 /// + /// Blocks: + /// [0..2]: {Nop, CondJump(2)}, cost 1+2 = 3 → entry_gas = 3 + /// [2..4]: {Nop, Return}, cost 1+3 = 4 + /// + /// CondJump(2): gas_taken = 4 (taken to block at 2), gas_fallthrough = 4 (fallthrough to 2) + /// (both paths lead to block [2..4] in this degenerate case) + /// /// Output: - /// 0: Charge(3) - /// 1: Nop - /// 2: CondJump(3) - /// 3: Charge(4) - /// 4: Nop - /// 5: Return + /// ops: [Nop, CondJump{2,taken=4,fall=4}, Nop, Return] + /// entry_gas: 3 #[test] fn blocks_have_correct_costs() { struct TestSchedule; impl GasSchedule for TestSchedule { - fn cost(&self, i: &TestOp) -> InstrCost { - InstrCost::constant(match i { + fn cost(&self, i: &TestOp) -> u64 { + match i { TestOp::Nop => 1, - TestOp::Jump(_) | TestOp::CondJump(_) => 2, + TestOp::Jump { .. } | TestOp::CondJump { .. } => 2, TestOp::Return => 3, - _ => 0, - }) + } } } let ops = vec![ TestOp::Nop, - TestOp::CondJump(2), + TestOp::CondJump { + target: 2, + gas_taken: 0, + gas_fallthrough: 0, + }, TestOp::Nop, TestOp::Return, ]; - let r: Vec = GasInstrumentor::new(TestSchedule).run(ops); - assert_eq!(r, vec![ - TestOp::Charge(3), + let (result, entry_gas) = GasInstrumentor::new(TestSchedule).run(ops); + assert_eq!(result, vec![ TestOp::Nop, - TestOp::CondJump(3), - TestOp::Charge(4), - TestOp::Nop, - TestOp::Return, - ]); - } - - /// Input: - /// 0: Nop — base: 5, dynamic: Some(ChargeDynamic(3)) - /// 1: Return — base: 5, dynamic: None - /// - /// Output: - /// 0: Charge(10) - /// 1: Nop - /// 2: ChargeDynamic(3) - /// 3: Return - #[test] - fn dynamic_charges_inserted_after_instruction() { - struct TestSchedule; - impl GasSchedule for TestSchedule { - fn cost(&self, op: &TestOp) -> InstrCost { - match op { - TestOp::Nop => InstrCost { - base: 5, - dynamic: Some(TestOp::ChargeDynamic(3)), - }, - _ => InstrCost::constant(5), - } - } - } - let ops = vec![TestOp::Nop, TestOp::Return]; - let r: Vec = GasInstrumentor::new(TestSchedule).run(ops); - assert_eq!(r, vec![ - TestOp::Charge(10), - TestOp::Nop, - TestOp::ChargeDynamic(3), - TestOp::Return, - ]); - } - - /// Dead code: Nop at index 1 is unreachable but still gets a Charge op. - /// - /// Input: - /// 0: Jump(2) — cost 1 - /// 1: Nop — cost 1 (dead) - /// 2: Return — cost 1 - /// - /// Output: - /// 0: Charge(1) - /// 1: Jump(4) - /// 2: Charge(1) - /// 3: Nop - /// 4: Charge(1) - /// 5: Return - #[test] - fn dead_code_block_still_instrumented() { - struct TestSchedule; - impl GasSchedule for TestSchedule { - fn cost(&self, _: &TestOp) -> InstrCost { - InstrCost::constant(1) - } - } - let ops = vec![TestOp::Jump(2), TestOp::Nop, TestOp::Return]; - let r: Vec = GasInstrumentor::new(TestSchedule).run(ops); - assert_eq!(r, vec![ - TestOp::Charge(1), - TestOp::Jump(4), - TestOp::Charge(1), + TestOp::CondJump { + target: 2, + gas_taken: 4, + gas_fallthrough: 4 + }, TestOp::Nop, - TestOp::Charge(1), TestOp::Return, ]); + assert_eq!(entry_gas, 3); } } diff --git a/third_party/move/mono-move/gas/src/lib.rs b/third_party/move/mono-move/gas/src/lib.rs index ef3cc0d9463..085d832d55a 100644 --- a/third_party/move/mono-move/gas/src/lib.rs +++ b/third_party/move/mono-move/gas/src/lib.rs @@ -5,35 +5,46 @@ //! //! This crate has **no dependency on any instruction set**. It defines the //! interfaces and the generic instrumentation pass; concrete instruction sets -//! (micro-ops, stackless IR, …) plug in by implementing four traits: +//! (micro-ops, stackless IR, …) plug in by implementing three traits: //! -//! | Trait | Purpose | -//! |---------------------------|------------------------------------------------| -//! | [`HasCfgInfo`] | Identifies basic-block boundaries | -//! | [`RemapTargets`] | Rewrites branch targets after charge insertion | -//! | [`GasSchedule`] | Maps each instruction to its [`InstrCost`] | -//! | [`GasMeteredInstruction`] | Constructs charge instructions within the ISA | +//! | Trait | Purpose | +//! |-----------------------|----------------------------------------------------------------| +//! | [`HasCfgInfo`] | Identifies basic-block boundaries | +//! | [`GasSchedule`] | Maps each instruction to its static base cost | +//! | [`ChargeOnJump`] | Writes destination-block costs into jump instructions | //! //! ## Integration //! -//! A new instruction set plugs in by adding a `Charge` variant to its -//! instruction type and implementing the four traits above. -//! [`GasInstrumentor::run`] then instruments any `Vec` at compile time. +//! A new instruction set plugs in by implementing the three traits above. +//! [`GasInstrumentor::run`] then instruments any `Vec` at compile time, +//! returning the annotated instruction sequence and the entry-block cost: //! -//! The interpreter handles the charge variant: +//! ```text +//! let (ops, entry_gas) = GasInstrumentor::new(MySchedule).run(ops); +//! // store entry_gas in the function's metadata +//! ``` +//! +//! At runtime the interpreter charges gas when executing a jump (before +//! entering the destination block) and at function entry (using `entry_gas`): //! //! ```text //! match instr { -//! MyOp::Charge { cost } => gas_meter.charge(cost)?, +//! MyOp::Jump { target, gas } => { gas_meter.charge(gas)?; pc = target; } +//! MyOp::CondJump { target, gas_taken, gas_fallthrough } => { +//! if cond { gas_meter.charge(gas_taken)?; pc = target; } +//! else { gas_meter.charge(gas_fallthrough)?; pc += 1; } +//! } //! ... //! } +//! // at function call time: +//! gas_meter.charge(callee.entry_gas)?; //! ``` pub mod cfg; pub mod instrument; pub use cfg::{compute_basic_blocks, BasicBlock, HasCfgInfo}; -pub use instrument::{GasInstrumentor, GasMeteredInstruction, RemapTargets}; +pub use instrument::{ChargeOnJump, GasInstrumentor}; use thiserror::Error; // --------------------------------------------------------------------------- @@ -78,43 +89,29 @@ impl GasMeter for SimpleGasMeter { } } -// --------------------------------------------------------------------------- -// Gas schedule -// --------------------------------------------------------------------------- - -/// The cost of a single instruction, as reported by [`GasSchedule::cost`]. -#[derive(Debug)] -pub struct InstrCost { - /// Accumulated into the enclosing `Charge` op for the basic block. - pub base: u64, +/// A no-op gas meter for testing. +pub struct NoOpGasMeter; - /// A fully-formed gas charge instruction to insert immediately after - /// the instruction, if any. - pub dynamic: Option, -} +impl GasMeter for NoOpGasMeter { + fn charge(&mut self, _amount: u64) -> Result<(), GasExhaustedError> { + Ok(()) + } -impl InstrCost { - pub fn constant(base: u64) -> Self { - Self { - base, - dynamic: None, - } + fn balance(&self) -> u64 { + u64::MAX } } -/// Maps instructions to their gas cost. +// --------------------------------------------------------------------------- +// Gas schedule +// --------------------------------------------------------------------------- + +/// Maps instructions to their static base gas cost. /// /// The instrumentation pass calls [`GasSchedule::cost`] once per instruction -/// and bakes all parameters into the emitted charge ops — the interpreter -/// never consults the schedule at runtime. -/// -/// # Constraint -/// -/// Branch instructions (those for which [`HasCfgInfo::branch_target`] returns -/// `Some`) must not have a dynamic cost component. The dynamic charge op is -/// inserted immediately after the instruction, so on the taken path execution -/// jumps away and the charge is never reached. For unconditional jumps it is -/// completely unreachable. +/// and accumulates the results into a block-level cost that is baked into each +/// block's entry jumps — the interpreter never consults the schedule at +/// runtime. pub trait GasSchedule { - fn cost(&self, instr: &I) -> InstrCost; + fn cost(&self, instr: &I) -> u64; } diff --git a/third_party/move/mono-move/global-context/src/context/executable.rs b/third_party/move/mono-move/global-context/src/context/executable.rs index 42e7bae2431..562fdf85ebb 100644 --- a/third_party/move/mono-move/global-context/src/context/executable.rs +++ b/third_party/move/mono-move/global-context/src/context/executable.rs @@ -180,7 +180,7 @@ impl<'a, 'guard, 'ctx> ExecutableBuilder<'a, 'guard, 'ctx> { specializer::lower::try_build_context(&module_ir.module, func_ir, &func_id_map)? { let micro_ops = specializer::lower::lower_function(func_ir, &ctx)?; - let micro_ops = GasInstrumentor::new(MicroOpGasSchedule).run(micro_ops); + let (micro_ops, entry_gas) = GasInstrumentor::new(MicroOpGasSchedule).run(micro_ops); // Compute frame layout. let args_size = ctx.home_slots[..func_ir.num_params as usize] @@ -204,6 +204,7 @@ impl<'a, 'guard, 'ctx> ExecutableBuilder<'a, 'guard, 'ctx> { args_size, args_and_locals_size, extended_frame_size, + entry_gas, // TODO: hardcoded for now. zero_frame: false, frame_layout: FrameLayoutInfo::empty(&self.arena), diff --git a/third_party/move/mono-move/programs/Cargo.toml b/third_party/move/mono-move/programs/Cargo.toml index 587ea0bbed5..c9dc6102c1e 100644 --- a/third_party/move/mono-move/programs/Cargo.toml +++ b/third_party/move/mono-move/programs/Cargo.toml @@ -14,7 +14,7 @@ rust-version = { workspace = true } [features] default = ["micro-op", "move-bytecode", "testing"] -micro-op = ["mono-move-alloc", "mono-move-core", "mono-move-runtime"] +micro-op = ["mono-move-alloc", "mono-move-core", "mono-move-gas", "mono-move-runtime"] move-bytecode = ["move-compiler-v2", "legacy-move-compiler", "move-binary-format", "tempfile"] testing = ["move-bytecode", "move-core-types", "move-vm-runtime", "move-vm-test-utils", "move-vm-types"] @@ -22,6 +22,7 @@ testing = ["move-bytecode", "move-core-types", "move-vm-runtime", "move-vm-test- legacy-move-compiler = { workspace = true, optional = true } mono-move-alloc = { workspace = true, optional = true } mono-move-core = { workspace = true, optional = true } +mono-move-gas = { workspace = true, optional = true } mono-move-runtime = { workspace = true, optional = true } move-binary-format = { workspace = true, optional = true } move-compiler-v2 = { workspace = true, optional = true } @@ -34,7 +35,6 @@ tempfile = { workspace = true, optional = true } [dev-dependencies] criterion = { workspace = true } -mono-move-gas = { workspace = true } [[bench]] name = "fib" @@ -51,3 +51,7 @@ harness = false [[bench]] name = "bst" harness = false + +[[bench]] +name = "match_sum" +harness = false diff --git a/third_party/move/mono-move/programs/benches/bst.rs b/third_party/move/mono-move/programs/benches/bst.rs index fb351332b96..d4a2f9e1e94 100644 --- a/third_party/move/mono-move/programs/benches/bst.rs +++ b/third_party/move/mono-move/programs/benches/bst.rs @@ -2,6 +2,10 @@ // Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion}; +use mono_move_gas::NoOpGasMeter; + +#[path = "helpers.rs"] +mod helpers; const N_OPS: u64 = 5000; const KEY_RANGE: u64 = 2500; @@ -28,15 +32,39 @@ fn bench_bst(c: &mut Criterion) { b.iter(|| native_run_ops(black_box(&ops))); }); + // plain (no gas instrumentation) let (functions, descriptors, _arena) = micro_op_bst(); // SAFETY: Exclusive access during bench setup; arena is alive. unsafe { mono_move_core::Function::resolve_calls(&functions) }; group.bench_function("micro_op", |b| { + b.iter_batched( + || { + let mut ctx = InterpreterContext::new(&descriptors, NoOpGasMeter, unsafe { + functions[6].unwrap().as_ref_unchecked() + }); + let vec_ptr = ctx + .alloc_u64_vec(mono_move_core::DescriptorId(0), &ops) + .unwrap(); + ctx.set_root_arg(0, &vec_ptr.to_le_bytes()); + ctx + }, + |mut ctx| ctx.run().unwrap(), + BatchSize::SmallInput, + ); + }); + + // with gas instrumentation + let (functions, _, _arena) = micro_op_bst(); + // SAFETY: Exclusive access during bench setup; arena is alive. + let (functions_gas, _arena) = unsafe { helpers::gas_instrument(&functions) }; + // SAFETY: Exclusive access during bench setup; arena is alive. + unsafe { mono_move_core::Function::resolve_calls(&functions_gas) }; + group.bench_function("micro_op/gas", |b| { b.iter_batched( || { let gas_meter = SimpleGasMeter::new(u64::MAX); let mut ctx = InterpreterContext::new(&descriptors, gas_meter, unsafe { - functions[6].unwrap().as_ref_unchecked() + functions_gas[6].unwrap().as_ref_unchecked() }); let vec_ptr = ctx .alloc_u64_vec(mono_move_core::DescriptorId(0), &ops) @@ -48,6 +76,7 @@ fn bench_bst(c: &mut Criterion) { BatchSize::SmallInput, ); }); + group.finish(); } diff --git a/third_party/move/mono-move/programs/benches/fib.rs b/third_party/move/mono-move/programs/benches/fib.rs index d7fbda5638a..d7ea56b62bd 100644 --- a/third_party/move/mono-move/programs/benches/fib.rs +++ b/third_party/move/mono-move/programs/benches/fib.rs @@ -3,10 +3,13 @@ use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion}; +#[path = "helpers.rs"] +mod helpers; + const N: u64 = 25; fn bench_fib(c: &mut Criterion) { - use mono_move_gas::SimpleGasMeter; + use mono_move_gas::{NoOpGasMeter, SimpleGasMeter}; use mono_move_programs::{ fib::{micro_op_fib, move_bytecode_fib, native_fib}, testing, @@ -24,15 +27,39 @@ fn bench_fib(c: &mut Criterion) { b.iter(|| black_box(native_fib(N))); }); + // plain (no gas instrumentation) let (functions, descriptors, _arena) = micro_op_fib(); // SAFETY: Exclusive access during bench setup; arena is alive. unsafe { mono_move_core::Function::resolve_calls(&functions) }; group.bench_function("micro_op", |b| { + b.iter_batched( + || { + let mut ctx = InterpreterContext::new(&descriptors, NoOpGasMeter, unsafe { + functions[0].unwrap().as_ref_unchecked() + }); + ctx.set_root_arg(0, &N.to_le_bytes()); + ctx + }, + |mut ctx| { + ctx.run().unwrap(); + black_box(ctx.root_result()) + }, + BatchSize::SmallInput, + ); + }); + + // with gas instrumentation + let (functions, _, _arena) = micro_op_fib(); + // SAFETY: Exclusive access during bench setup; arena is alive. + let (functions_gas, _arena) = unsafe { helpers::gas_instrument(&functions) }; + // SAFETY: Exclusive access during bench setup; arena is alive. + unsafe { mono_move_core::Function::resolve_calls(&functions_gas) }; + group.bench_function("micro_op/gas", |b| { b.iter_batched( || { let gas_meter = SimpleGasMeter::new(u64::MAX); let mut ctx = InterpreterContext::new(&descriptors, gas_meter, unsafe { - functions[0].unwrap().as_ref_unchecked() + functions_gas[0].unwrap().as_ref_unchecked() }); ctx.set_root_arg(0, &N.to_le_bytes()); ctx @@ -44,6 +71,7 @@ fn bench_fib(c: &mut Criterion) { BatchSize::SmallInput, ); }); + group.finish(); } diff --git a/third_party/move/mono-move/programs/benches/helpers.rs b/third_party/move/mono-move/programs/benches/helpers.rs new file mode 100644 index 00000000000..858072d7ef1 --- /dev/null +++ b/third_party/move/mono-move/programs/benches/helpers.rs @@ -0,0 +1,55 @@ +// Copyright (c) Aptos Foundation +// Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE + +//! Shared bench helpers. Included via `#[path = "helpers.rs"] mod helpers;` +//! in each bench binary. Not listed as a `[[bench]]` target, so it is never +//! compiled standalone. + +use mono_move_alloc::{ExecutableArena, ExecutableArenaPtr}; +use mono_move_core::{FrameLayoutInfo, Function, MicroOpGasSchedule, SortedSafePointEntries}; +use mono_move_gas::GasInstrumentor; + +/// Build a gas-instrumented copy of `func_ptrs`, re-allocated in a fresh arena. +/// +/// The caller should build the program fresh (without calling +/// `Function::resolve_calls`) before passing it here, so the code still +/// contains `CallFunc` (index-based) ops. After this call, invoke +/// `Function::resolve_calls` on the returned table to patch those ops to +/// direct pointers into the instrumented arena. +/// +/// Frame layouts are re-created as empty; these benchmark programs do not +/// trigger GC, so the omission has no effect on execution. +/// +/// # Safety +/// +/// Each pointer in `func_ptrs` must be valid (the owning arena must outlive +/// this call). +pub unsafe fn gas_instrument( + func_ptrs: &[Option>], +) -> (Vec>>, ExecutableArena) { + let arena = ExecutableArena::new(); + let instrumentor = GasInstrumentor::new(MicroOpGasSchedule); + let new_fns = func_ptrs + .iter() + .map(|f| { + let fp = (*f)?; + // SAFETY: caller guarantees the pointer is valid. + let func = unsafe { fp.as_ref_unchecked() }; + let raw = unsafe { func.code.as_ref_unchecked() }; + let (instrumented, entry_gas) = instrumentor.run(raw.to_vec()); + let code = arena.alloc_slice_copy(&instrumented); + Some(arena.alloc(Function { + name: func.name, + code, + entry_gas, + args_size: func.args_size, + args_and_locals_size: func.args_and_locals_size, + extended_frame_size: func.extended_frame_size, + zero_frame: func.zero_frame, + frame_layout: FrameLayoutInfo::empty(&arena), + safe_point_layouts: SortedSafePointEntries::empty(&arena), + })) + }) + .collect(); + (new_fns, arena) +} diff --git a/third_party/move/mono-move/programs/benches/match_sum.rs b/third_party/move/mono-move/programs/benches/match_sum.rs new file mode 100644 index 00000000000..ac2c42493e6 --- /dev/null +++ b/third_party/move/mono-move/programs/benches/match_sum.rs @@ -0,0 +1,97 @@ +// Copyright (c) Aptos Foundation +// Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE + +use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion}; + +#[path = "helpers.rs"] +mod helpers; + +const N: u64 = 1_000_000; + +fn bench_match_sum(c: &mut Criterion) { + use mono_move_gas::{NoOpGasMeter, SimpleGasMeter}; + use mono_move_programs::{ + match_sum::{micro_op_match_sum, move_bytecode_match_sum, native_match_sum}, + testing, + }; + use mono_move_runtime::InterpreterContext; + + // -- native & micro_op ------------------------------------------------- + { + let mut group = c.benchmark_group("match_sum"); + group + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(3)); + + group.bench_function("native", |b| { + b.iter(|| black_box(native_match_sum(N))); + }); + + // plain (no gas instrumentation) + let (functions, descriptors, _arena) = micro_op_match_sum(); + group.bench_function("micro_op", |b| { + b.iter_batched( + || { + let mut ctx = InterpreterContext::new(&descriptors, NoOpGasMeter, unsafe { + functions[0].as_ref_unchecked() + }); + ctx.set_root_arg(0, &N.to_le_bytes()); + ctx + }, + |mut ctx| { + ctx.run().unwrap(); + black_box(ctx.root_result()) + }, + BatchSize::SmallInput, + ); + }); + + // with gas instrumentation + let (functions, _, _arena) = micro_op_match_sum(); + let wrapped = functions.iter().map(|f| Some(*f)).collect::>(); + // SAFETY: Exclusive access during bench setup; arena is alive. + let (functions_gas, _arena) = unsafe { helpers::gas_instrument(&wrapped) }; + group.bench_function("micro_op/gas", |b| { + b.iter_batched( + || { + let gas_meter = SimpleGasMeter::new(u64::MAX); + let mut ctx = InterpreterContext::new(&descriptors, gas_meter, unsafe { + functions_gas[0].unwrap().as_ref_unchecked() + }); + ctx.set_root_arg(0, &N.to_le_bytes()); + ctx + }, + |mut ctx| { + ctx.run().unwrap(); + black_box(ctx.root_result()) + }, + BatchSize::SmallInput, + ); + }); + + group.finish(); + } + + // -- move_vm ----------------------------------------------------------- + { + let mut group = c.benchmark_group("match_sum"); + group + .sample_size(10) + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(3)); + + let module = move_bytecode_match_sum(); + testing::with_loaded_move_function(&module, "match_sum", |env| { + group.bench_function("move_vm", |b| { + b.iter(|| { + let result = env.run(vec![testing::arg_u64(N)]); + black_box(testing::return_u64(&result)) + }); + }); + }); + group.finish(); + } +} + +criterion_group!(benches, bench_match_sum); +criterion_main!(benches); diff --git a/third_party/move/mono-move/programs/benches/merge_sort.rs b/third_party/move/mono-move/programs/benches/merge_sort.rs index 246f4689144..469147f88d3 100644 --- a/third_party/move/mono-move/programs/benches/merge_sort.rs +++ b/third_party/move/mono-move/programs/benches/merge_sort.rs @@ -3,10 +3,13 @@ use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion}; +#[path = "helpers.rs"] +mod helpers; + const N: u64 = 1000; fn bench_merge_sort(c: &mut Criterion) { - use mono_move_gas::SimpleGasMeter; + use mono_move_gas::{NoOpGasMeter, SimpleGasMeter}; use mono_move_programs::{ merge_sort::{ micro_op_merge_sort, move_bytecode_merge_sort, native_merge_sort, shuffled_range, @@ -35,15 +38,39 @@ fn bench_merge_sort(c: &mut Criterion) { ); }); + // plain (no gas instrumentation) let (functions, descriptors, _arena) = micro_op_merge_sort(); // SAFETY: Exclusive access during bench setup; arena is alive. unsafe { mono_move_core::Function::resolve_calls(&functions) }; group.bench_function("micro_op", |b| { + b.iter_batched( + || { + let mut ctx = InterpreterContext::new(&descriptors, NoOpGasMeter, unsafe { + functions[0].unwrap().as_ref_unchecked() + }); + let vec_ptr = ctx + .alloc_u64_vec(mono_move_core::DescriptorId(0), &input) + .unwrap(); + ctx.set_root_arg(0, &vec_ptr.to_le_bytes()); + ctx + }, + |mut ctx| ctx.run().unwrap(), + BatchSize::SmallInput, + ); + }); + + // with gas instrumentation + let (functions, _, _arena) = micro_op_merge_sort(); + // SAFETY: Exclusive access during bench setup; arena is alive. + let (functions_gas, _arena) = unsafe { helpers::gas_instrument(&functions) }; + // SAFETY: Exclusive access during bench setup; arena is alive. + unsafe { mono_move_core::Function::resolve_calls(&functions_gas) }; + group.bench_function("micro_op/gas", |b| { b.iter_batched( || { let gas_meter = SimpleGasMeter::new(u64::MAX); let mut ctx = InterpreterContext::new(&descriptors, gas_meter, unsafe { - functions[0].unwrap().as_ref_unchecked() + functions_gas[0].unwrap().as_ref_unchecked() }); let vec_ptr = ctx .alloc_u64_vec(mono_move_core::DescriptorId(0), &input) @@ -55,6 +82,7 @@ fn bench_merge_sort(c: &mut Criterion) { BatchSize::SmallInput, ); }); + group.finish(); } diff --git a/third_party/move/mono-move/programs/benches/nested_loop.rs b/third_party/move/mono-move/programs/benches/nested_loop.rs index 724a564b6a8..978a13da92d 100644 --- a/third_party/move/mono-move/programs/benches/nested_loop.rs +++ b/third_party/move/mono-move/programs/benches/nested_loop.rs @@ -3,10 +3,13 @@ use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion}; +#[path = "helpers.rs"] +mod helpers; + const N: u64 = 1000; fn bench_nested_loop(c: &mut Criterion) { - use mono_move_gas::SimpleGasMeter; + use mono_move_gas::{NoOpGasMeter, SimpleGasMeter}; use mono_move_programs::{ nested_loop::{micro_op_nested_loop, move_bytecode_nested_loop, native_nested_loop}, testing, @@ -24,13 +27,36 @@ fn bench_nested_loop(c: &mut Criterion) { b.iter(|| black_box(native_nested_loop(N))); }); + // plain (no gas instrumentation) let (functions, descriptors, _arena) = micro_op_nested_loop(); group.bench_function("micro_op", |b| { + b.iter_batched( + || { + let mut ctx = InterpreterContext::new(&descriptors, NoOpGasMeter, unsafe { + functions[0].as_ref_unchecked() + }); + ctx.set_root_arg(0, &N.to_le_bytes()); + ctx + }, + |mut ctx| { + ctx.run().unwrap(); + black_box(ctx.root_result()) + }, + BatchSize::SmallInput, + ); + }); + + // with gas instrumentation + let (functions, _, _arena) = micro_op_nested_loop(); + let wrapped = functions.iter().map(|f| Some(*f)).collect::>(); + // SAFETY: Exclusive access during bench setup; arena is alive. + let (functions_gas, _arena) = unsafe { helpers::gas_instrument(&wrapped) }; + group.bench_function("micro_op/gas", |b| { b.iter_batched( || { let gas_meter = SimpleGasMeter::new(u64::MAX); let mut ctx = InterpreterContext::new(&descriptors, gas_meter, unsafe { - functions[0].as_ref_unchecked() + functions_gas[0].unwrap().as_ref_unchecked() }); ctx.set_root_arg(0, &N.to_le_bytes()); ctx @@ -42,6 +68,7 @@ fn bench_nested_loop(c: &mut Criterion) { BatchSize::SmallInput, ); }); + group.finish(); } diff --git a/third_party/move/mono-move/programs/src/bst.rs b/third_party/move/mono-move/programs/src/bst.rs index 07c00776fec..f818f8006b7 100644 --- a/third_party/move/mono-move/programs/src/bst.rs +++ b/third_party/move/mono-move/programs/src/bst.rs @@ -355,6 +355,7 @@ mod micro_op { extended_frame_size: 24 + FRAME_METADATA_SIZE, zero_frame: true, frame_layout: FrameLayoutInfo::new(arena, vec![FO(bst), FO(nodes), FO(free_list)]), + entry_gas: 0, safe_point_layouts: SortedSafePointEntries::empty(arena), }) } @@ -390,21 +391,21 @@ mod micro_op { Op::struct_load8(FO(bst), BST_ROOT, FO(root)), // 2 Move8 { dst: FO(idx), src: FO(root) }, // 3: idx = root // -- LOOP (4) -- - JumpGreaterEqualU64Imm { target: CO(15), src: FO(idx), imm: NULL }, // 4: NULL? → NONE + JumpGreaterEqualU64Imm { target: CO(15), src: FO(idx), imm: NULL, gas_taken: 0, gas_fallthrough: 0 }, // 4: NULL? → NONE VecLoadElem { dst: FO(node_key), vec_ref: FO(nodes_ref), idx: FO(idx), elem_size: NODE_SIZE }, // 5: node = nodes[idx] - JumpLessU64 { target: CO(11), lhs: FO(key), rhs: FO(node_key) }, // 6: key < node.key → LEFT - JumpLessU64 { target: CO(13), lhs: FO(node_key), rhs: FO(key) }, // 7: node.key < key → RIGHT + JumpLessU64 { target: CO(11), lhs: FO(key), rhs: FO(node_key), gas_taken: 0, gas_fallthrough: 0 }, // 6: key < node.key → LEFT + JumpLessU64 { target: CO(13), lhs: FO(node_key), rhs: FO(key), gas_taken: 0, gas_fallthrough: 0 }, // 7: node.key < key → RIGHT // EQUAL (8) StoreImm8 { dst: FO(tag), imm: 1 }, // 8 Move8 { dst: FO(value), src: FO(node_val) }, // 9 Return, // 10 // GO_LEFT (11) Move8 { dst: FO(idx), src: FO(node_left) }, // 11 - Jump { target: CO(4) }, // 12 + Jump { target: CO(4), gas: 0 }, // 12 // GO_RIGHT (13) Move8 { dst: FO(idx), src: FO(node_right) }, // 13 - Jump { target: CO(4) }, // 14 + Jump { target: CO(4), gas: 0 }, // 14 // NONE (15) StoreImm8 { dst: FO(tag), imm: 0 }, // 15 StoreImm8 { dst: FO(value), imm: 0 }, // 16 @@ -421,6 +422,7 @@ mod micro_op { extended_frame_size: 96 + FRAME_METADATA_SIZE, zero_frame: false, frame_layout: FrameLayoutInfo::new(arena, vec![FO(bst), FO(bst_ref), FO(nodes_ref)]), + entry_gas: 0, safe_point_layouts: SortedSafePointEntries::empty(arena), }) } @@ -466,7 +468,7 @@ mod micro_op { Op::struct_load8(FO(bst), BST_ROOT, FO(root)), // 2 // -- if root != NULL → LOOP_SETUP; else fall through to INSERT_ROOT -- JumpLessU64Imm { target: CO(10), - src: FO(root), imm: NULL }, // 3: → LOOP_SETUP + src: FO(root), imm: NULL, gas_taken: 0, gas_fallthrough: 0 }, // 3: → LOOP_SETUP // -- INSERT_ROOT (4): bst.root = alloc_node(bst, key, value); return -- Move8 { dst: FO(c0), src: FO(bst) }, // 4 Move8 { dst: FO(c1), src: FO(key) }, // 5 @@ -481,10 +483,10 @@ mod micro_op { idx: FO(idx), elem_size: NODE_SIZE }, // 11 // -- key < node_key? (GO_LEFT falls through, skip if >=) -- JumpGreaterEqualU64 { target: CO(23), - lhs: FO(key), rhs: FO(node_key) }, // 12: key >= node_key → NOT_LESS + lhs: FO(key), rhs: FO(node_key), gas_taken: 0, gas_fallthrough: 0 }, // 12: key >= node_key → NOT_LESS // GO_LEFT (13): key < node_key JumpLessU64Imm { target: CO(21), - src: FO(node_left), imm: NULL }, // 13: left != NULL → CONTINUE_LEFT + src: FO(node_left), imm: NULL, gas_taken: 0, gas_fallthrough: 0 }, // 13: left != NULL → CONTINUE_LEFT // INSERT_LEFT (14): node.left = alloc_node(...); store node; return Move8 { dst: FO(c0), src: FO(bst) }, // 14 Move8 { dst: FO(c1), src: FO(key) }, // 15 @@ -496,13 +498,13 @@ mod micro_op { Return, // 20 // CONTINUE_LEFT (21) Move8 { dst: FO(idx), src: FO(node_left) }, // 21 - Jump { target: CO(11) }, // 22 + Jump { target: CO(11), gas: 0 }, // 22 // -- NOT_LESS (23): key > node_key? -- JumpGreaterEqualU64 { target: CO(34), - lhs: FO(node_key), rhs: FO(key) }, // 23: node_key >= key → EQUAL + lhs: FO(node_key), rhs: FO(key), gas_taken: 0, gas_fallthrough: 0 }, // 23: node_key >= key → EQUAL // GO_RIGHT (24): key > node_key JumpLessU64Imm { target: CO(32), - src: FO(node_right), imm: NULL }, // 24: right != NULL → CONTINUE_RIGHT + src: FO(node_right), imm: NULL, gas_taken: 0, gas_fallthrough: 0 }, // 24: right != NULL → CONTINUE_RIGHT // INSERT_RIGHT (25): node.right = alloc_node(...); store node; return Move8 { dst: FO(c0), src: FO(bst) }, // 25 Move8 { dst: FO(c1), src: FO(key) }, // 26 @@ -514,7 +516,7 @@ mod micro_op { Return, // 31 // CONTINUE_RIGHT (32) Move8 { dst: FO(idx), src: FO(node_right) }, // 32 - Jump { target: CO(11) }, // 33 + Jump { target: CO(11), gas: 0 }, // 33 // -- EQUAL (34): node.value = value; store node; return -- Move8 { dst: FO(node_val), src: FO(value) }, // 34 VecStoreElem { vec_ref: FO(nodes_ref), idx: FO(idx), @@ -532,6 +534,7 @@ mod micro_op { extended_frame_size: (c2 + 8) as usize, zero_frame: true, frame_layout: FrameLayoutInfo::new(arena, vec![FO(bst), FO(bst_ref), FO(nodes_ref)]), + entry_gas: 0, safe_point_layouts: SortedSafePointEntries::empty(arena), }) } @@ -580,12 +583,12 @@ mod micro_op { StoreImm8 { dst: FO(new_node_right), imm: NULL }, // 6 // Check free_list VecLen { dst: FO(fl_len), vec_ref: FO(free_list_ref) }, // 7 - JumpNotZeroU64 { target: CO(12), src: FO(fl_len) }, // 8: → POP + JumpNotZeroU64 { target: CO(12), src: FO(fl_len), gas_taken: 0, gas_fallthrough: 0 }, // 8: → POP // PUSH path: idx = nodes.len(); nodes.push(new_node) VecLen { dst: FO(idx), vec_ref: FO(nodes_ref) }, // 9 VecPushBack { vec_ref: FO(nodes_ref), elem: FO(new_node), elem_size: NODE_SIZE, descriptor_id: DESC_TRIVIAL }, // 10 - Jump { target: CO(14) }, // 11: → DONE + Jump { target: CO(14), gas: 0 }, // 11: → DONE // POP path (12): idx = free_list.pop(); nodes[idx] = new_node VecPopBack { dst: FO(idx), vec_ref: FO(free_list_ref), elem_size: 8 }, // 12 @@ -611,6 +614,7 @@ mod micro_op { FO(nodes_ref), FO(free_list_ref), ]), + entry_gas: 0, safe_point_layouts: SortedSafePointEntries::empty(arena), }) } @@ -656,30 +660,30 @@ mod micro_op { Move8 { dst: FO(idx), src: FO(root) }, // 4 // -- LOOP (5): while idx != NULL -- JumpGreaterEqualU64Imm { target: CO(27), - src: FO(idx), imm: NULL }, // 5: → DONE + src: FO(idx), imm: NULL, gas_taken: 0, gas_fallthrough: 0 }, // 5: → DONE VecLoadElem { dst: FO(node), vec_ref: FO(nodes_ref), idx: FO(idx), elem_size: NODE_SIZE }, // 6 // -- key < node_key? -- JumpGreaterEqualU64 { target: CO(11), - lhs: FO(key), rhs: FO(node_key) }, // 7: key >= → NOT_LESS + lhs: FO(key), rhs: FO(node_key), gas_taken: 0, gas_fallthrough: 0 }, // 7: key >= → NOT_LESS // GO_LEFT: key < node_key Move8 { dst: FO(parent), src: FO(idx) }, // 8 Move8 { dst: FO(idx), src: FO(node_left) }, // 9 - Jump { target: CO(5) }, // 10 + Jump { target: CO(5), gas: 0 }, // 10 // -- NOT_LESS (11): key > node_key? -- JumpGreaterEqualU64 { target: CO(15), - lhs: FO(node_key), rhs: FO(key) }, // 11: node_key >= key → EQUAL + lhs: FO(node_key), rhs: FO(key), gas_taken: 0, gas_fallthrough: 0 }, // 11: node_key >= key → EQUAL // GO_RIGHT: key > node_key Move8 { dst: FO(parent), src: FO(idx) }, // 12 Move8 { dst: FO(idx), src: FO(node_right) }, // 13 - Jump { target: CO(5) }, // 14 + Jump { target: CO(5), gas: 0 }, // 14 // -- EQUAL (15): remove_node(bst, idx) → c0 holds replacement -- Move8 { dst: FO(c0), src: FO(bst) }, // 15 Move8 { dst: FO(c1), src: FO(idx) }, // 16 CallFunc { func_id: remove_node_id }, // 17 // -- if parent != NULL → HAS_PARENT -- JumpLessU64Imm { target: CO(21), - src: FO(parent), imm: NULL }, // 18: → HAS_PARENT + src: FO(parent), imm: NULL, gas_taken: 0, gas_fallthrough: 0 }, // 18: → HAS_PARENT // UPDATE_ROOT: parent == NULL, bst.root = replacement (c0) Op::struct_store8(FO(bst), BST_ROOT, FO(c0)), // 19 Return, // 20 @@ -687,10 +691,10 @@ mod micro_op { VecLoadElem { dst: FO(node), vec_ref: FO(nodes_ref), idx: FO(parent), elem_size: NODE_SIZE }, // 21 JumpNotEqualU64 { target: CO(25), - lhs: FO(node_left), rhs: FO(idx) }, // 22: → UPDATE_RIGHT + lhs: FO(node_left), rhs: FO(idx), gas_taken: 0, gas_fallthrough: 0 }, // 22: → UPDATE_RIGHT // UPDATE_LEFT: parent.left = replacement (c0) Move8 { dst: FO(node_left), src: FO(c0) }, // 23 - Jump { target: CO(26) }, // 24: → STORE_PARENT + Jump { target: CO(26), gas: 0 }, // 24: → STORE_PARENT // UPDATE_RIGHT (25): parent.right = replacement (c0) Move8 { dst: FO(node_right), src: FO(c0) }, // 25 // -- STORE_PARENT (26): shared epilogue -- @@ -709,6 +713,7 @@ mod micro_op { extended_frame_size: (c1 + 8) as usize, zero_frame: true, frame_layout: FrameLayoutInfo::new(arena, vec![FO(bst), FO(bst_ref), FO(nodes_ref)]), + entry_gas: 0, safe_point_layouts: SortedSafePointEntries::empty(arena), }) } @@ -757,25 +762,25 @@ mod micro_op { Move8 { dst: FO(right), src: FO(scratch_right) }, // 5 // -- if left == NULL: free idx, return right -- JumpLessU64Imm { target: CO(9), - src: FO(left), imm: NULL }, // 6: left != NULL → HAS_LEFT + src: FO(left), imm: NULL, gas_taken: 0, gas_fallthrough: 0 }, // 6: left != NULL → HAS_LEFT Move8 { dst: FO(result), src: FO(right) }, // 7 - Jump { target: CO(33) }, // 8: → FREE_RETURN + Jump { target: CO(33), gas: 0 }, // 8: → FREE_RETURN // -- HAS_LEFT (9): if right == NULL: free idx, return left -- JumpLessU64Imm { target: CO(12), - src: FO(right), imm: NULL }, // 9: right != NULL → HAS_BOTH + src: FO(right), imm: NULL, gas_taken: 0, gas_fallthrough: 0 }, // 9: right != NULL → HAS_BOTH Move8 { dst: FO(result), src: FO(left) }, // 10 - Jump { target: CO(33) }, // 11: → FREE_RETURN + Jump { target: CO(33), gas: 0 }, // 11: → FREE_RETURN // -- HAS_BOTH (12): load nodes[right], check right.left -- VecLoadElem { dst: FO(scratch), vec_ref: FO(nodes_ref), idx: FO(right), elem_size: NODE_SIZE }, // 12 JumpLessU64Imm { target: CO(18), - src: FO(scratch_left), imm: NULL }, // 13: right.left != NULL → DEEP + src: FO(scratch_left), imm: NULL, gas_taken: 0, gas_fallthrough: 0 }, // 13: right.left != NULL → DEEP // SIMPLE_SUCCESSOR (14): right.left == NULL, right adopts left Move8 { dst: FO(scratch_left), src: FO(left) }, // 14 VecStoreElem { vec_ref: FO(nodes_ref), idx: FO(right), src: FO(scratch), elem_size: NODE_SIZE }, // 15 Move8 { dst: FO(result), src: FO(right) }, // 16 - Jump { target: CO(33) }, // 17: → FREE_RETURN + Jump { target: CO(33), gas: 0 }, // 17: → FREE_RETURN // -- DEEP_SUCCESSOR (18): walk left to find in-order successor -- Move8 { dst: FO(parent), src: FO(right) }, // 18 Move8 { dst: FO(cur), src: FO(scratch_left) }, // 19 @@ -783,10 +788,10 @@ mod micro_op { VecLoadElem { dst: FO(scratch), vec_ref: FO(nodes_ref), idx: FO(cur), elem_size: NODE_SIZE }, // 20 JumpGreaterEqualU64Imm { target: CO(25), - src: FO(scratch_left), imm: NULL }, // 21: cur.left == NULL → WALK_DONE + src: FO(scratch_left), imm: NULL, gas_taken: 0, gas_fallthrough: 0 }, // 21: cur.left == NULL → WALK_DONE Move8 { dst: FO(parent), src: FO(cur) }, // 22 Move8 { dst: FO(cur), src: FO(scratch_left) }, // 23 - Jump { target: CO(20) }, // 24 + Jump { target: CO(20), gas: 0 }, // 24 // -- WALK_DONE (25): scratch = nodes[cur] -- // Save cur.right, then set cur's children to left/right from removed node Move8 { dst: FO(cur_right), src: FO(scratch_right) }, // 25 @@ -822,6 +827,7 @@ mod micro_op { FO(nodes_ref), FO(free_list_ref), ]), + entry_gas: 0, safe_point_layouts: SortedSafePointEntries::empty(arena), }) } @@ -867,7 +873,7 @@ mod micro_op { StoreImm8 { dst: FO(i), imm: 0 }, // 4 // -- LOOP (5) -- JumpGreaterEqualU64 { target: CO(28), - lhs: FO(i), rhs: FO(len) }, // 5: → DONE + lhs: FO(i), rhs: FO(len), gas_taken: 0, gas_fallthrough: 0 }, // 5: → DONE // Load triple VecLoadElem { dst: FO(op_code), vec_ref: FO(ops_ref), idx: FO(i), elem_size: 8 }, // 6 @@ -879,28 +885,28 @@ mod micro_op { idx: FO(i), elem_size: 8 }, // 10 AddU64Imm { dst: FO(i), src: FO(i), imm: 1 }, // 11 // -- Dispatch -- - JumpNotZeroU64 { target: CO(18), src: FO(op_code) }, // 12: → CHECK_GET + JumpNotZeroU64 { target: CO(18), src: FO(op_code), gas_taken: 0, gas_fallthrough: 0 }, // 12: → CHECK_GET // INSERT (13) Move8 { dst: FO(c0), src: FO(bst) }, // 13 Move8 { dst: FO(c1), src: FO(key) }, // 14 Move8 { dst: FO(c2), src: FO(value) }, // 15 CallFunc { func_id: FN_INSERT as u32 }, // 16 - Jump { target: CO(5) }, // 17 + Jump { target: CO(5), gas: 0 }, // 17 // CHECK_GET (18) JumpGreaterEqualU64Imm { target: CO(24), - src: FO(op_code), imm: 2 }, // 18: → REMOVE + src: FO(op_code), imm: 2, gas_taken: 0, gas_fallthrough: 0 }, // 18: → REMOVE // GET (19) Move8 { dst: FO(c0), src: FO(bst) }, // 19 Move8 { dst: FO(c1), src: FO(key) }, // 20 CallFunc { func_id: FN_GET as u32 }, // 21 - Jump { target: CO(5) }, // 22 + Jump { target: CO(5), gas: 0 }, // 22 // skip (23) — padding for alignment, shouldn't be reached - Jump { target: CO(5) }, // 23 + Jump { target: CO(5), gas: 0 }, // 23 // REMOVE (24) Move8 { dst: FO(c0), src: FO(bst) }, // 24 Move8 { dst: FO(c1), src: FO(key) }, // 25 CallFunc { func_id: FN_REMOVE as u32 }, // 26 - Jump { target: CO(5) }, // 27 + Jump { target: CO(5), gas: 0 }, // 27 // DONE (28) Return, // 28 ]; @@ -915,6 +921,7 @@ mod micro_op { extended_frame_size: (c2 + 8) as usize, zero_frame: true, frame_layout: FrameLayoutInfo::new(arena, vec![FO(ops), FO(bst), FO(ops_ref)]), + entry_gas: 0, safe_point_layouts: SortedSafePointEntries::empty(arena), }) } diff --git a/third_party/move/mono-move/programs/src/fib.rs b/third_party/move/mono-move/programs/src/fib.rs index b91a89ea6a9..99309346ddc 100644 --- a/third_party/move/mono-move/programs/src/fib.rs +++ b/third_party/move/mono-move/programs/src/fib.rs @@ -62,11 +62,11 @@ mod micro_op { #[rustfmt::skip] let code = vec![ // if n != 0 goto CHECKGE2 - JumpNotZeroU64 { target: CO(3), src: FO(n) }, + JumpNotZeroU64 { target: CO(3), src: FO(n), gas_taken: 0, gas_fallthrough: 0 }, StoreImm8 { dst: FO(result), imm: 0 }, Return, // CHECKGE2: if n >= 2 goto RECURSE - JumpGreaterEqualU64Imm { target: CO(6), src: FO(n), imm: 2 }, + JumpGreaterEqualU64Imm { target: CO(6), src: FO(n), imm: 2, gas_taken: 0, gas_fallthrough: 0 }, StoreImm8 { dst: FO(result), imm: 1 }, Return, // RECURSE: tmp = fib(n - 1) @@ -91,6 +91,7 @@ mod micro_op { extended_frame_size: (callee_n + 8) as usize, zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), + entry_gas: 0, safe_point_layouts: SortedSafePointEntries::empty(&arena), }); diff --git a/third_party/move/mono-move/programs/src/lib.rs b/third_party/move/mono-move/programs/src/lib.rs index 6703dbdb674..971c402b8a5 100644 --- a/third_party/move/mono-move/programs/src/lib.rs +++ b/third_party/move/mono-move/programs/src/lib.rs @@ -10,6 +10,7 @@ pub mod bst; pub mod fib; +pub mod match_sum; pub mod merge_sort; pub mod nested_loop; #[cfg(feature = "testing")] diff --git a/third_party/move/mono-move/programs/src/match_sum.rs b/third_party/move/mono-move/programs/src/match_sum.rs new file mode 100644 index 00000000000..50f4125ebbe --- /dev/null +++ b/third_party/move/mono-move/programs/src/match_sum.rs @@ -0,0 +1,202 @@ +// Copyright (c) Aptos Foundation +// Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE + +//! Match sum — O(n) loop with a 4-arm match inside. +//! +//! The inner body computes `r = i % 4` and branches to one of four arms, +//! all converging to a single merge block. This creates a "wide diamond" +//! CFG shape (multiple basic blocks with edges to the same successor), +//! which stresses gas instrumentation at basic-block boundaries. + +/// Test cases: (input, expected output). +/// +/// Each full cycle of 4 iterations contributes 10+20+30+40 = 100. +pub const MATCH_SUM_CASES: &[(u64, u64)] = &[ + (0, 0), + (1, 10), + (2, 30), + (3, 60), + (4, 100), + (8, 200), + (100, 2500), +]; + +// --------------------------------------------------------------------------- +// Native Rust +// --------------------------------------------------------------------------- + +pub fn native_match_sum(n: u64) -> u64 { + let mut sum = 0u64; + let mut i = 0u64; + while i < n { + sum += match i % 4 { + 0 => 10, + 1 => 20, + 2 => 30, + _ => 40, + }; + i += 1; + } + sum +} + +// --------------------------------------------------------------------------- +// Micro-op +// --------------------------------------------------------------------------- + +/// Pseudocode: +/// fn match_sum(n: u64) -> u64 { +/// let sum: u64 = 0; +/// let i: u64 = 0; +/// while i < n { +/// match i % 4 { +/// 0 => sum += 10, +/// 1 => sum += 20, +/// 2 => sum += 30, +/// _ => sum += 40, +/// } +/// i += 1; +/// } +/// sum +/// } +/// +/// Frame layout: +/// [0] n (arg) / result +/// [8] sum +/// [16] i +/// [24] r (i % 4) +/// [32] const4 +/// +/// CFG shape — "wide diamond": +/// +/// LOOP ──► BODY ──► head ──► CASE0 ────────────────────╮ +/// └─► GE1 ──► CASE1 ───────────┤ +/// └─► GE2 ──► CASE2 ──┤ +/// └─► CASE3 ──┤ (fallthrough) +/// │ +/// MERGE ──► LOOP / END +/// +/// All four arms (CASE0–CASE3) jump (or fall through) to MERGE, giving it +/// four predecessors — the "wide diamond" this benchmark is designed to test. +#[cfg(feature = "micro-op")] +mod micro_op { + use mono_move_alloc::{ExecutableArena, ExecutableArenaPtr, GlobalArenaPtr}; + use mono_move_core::{ + CodeOffset as CO, FrameLayoutInfo, FrameOffset as FO, Function, MicroOp::*, + SortedSafePointEntries, + }; + use mono_move_runtime::ObjectDescriptor; + + pub fn program() -> ( + Vec>, + Vec, + ExecutableArena, + ) { + let arena = ExecutableArena::new(); + let n = 0u32; + let sum = 8u32; + let i = 16u32; + let r = 24u32; + let c4 = 32u32; + let args_and_locals_size = 40u32; + + #[rustfmt::skip] + let code = vec![ + StoreImm8 { dst: FO(sum), imm: 0 }, // 0: sum = 0 + StoreImm8 { dst: FO(i), imm: 0 }, // 1: i = 0 + StoreImm8 { dst: FO(c4), imm: 4 }, // 2: const4 = 4 + + // LOOP (3): if i < n goto BODY else goto END + JumpLessU64 { target: CO(6), lhs: FO(i), rhs: FO(n), gas_taken: 0, gas_fallthrough: 0 }, // 3 + Move8 { dst: FO(n), src: FO(sum) }, // 4: result = sum + Return, // 5 + + // BODY (6): r = i % 4 + ModU64 { dst: FO(r), lhs: FO(i), rhs: FO(c4) }, // 6 + + // if r >= 1 goto GE1 else CASE0 + JumpGreaterEqualU64Imm { target: CO(10), src: FO(r), imm: 1, gas_taken: 0, gas_fallthrough: 0 }, // 7 + // CASE0 (8): sum += 10; goto MERGE + AddU64Imm { dst: FO(sum), src: FO(sum), imm: 10 }, // 8 + Jump { target: CO(17), gas: 0 }, // 9 + + // GE1 (10): if r >= 2 goto GE2 else CASE1 + JumpGreaterEqualU64Imm { target: CO(13), src: FO(r), imm: 2, gas_taken: 0, gas_fallthrough: 0 }, // 10 + // CASE1 (11): sum += 20; goto MERGE + AddU64Imm { dst: FO(sum), src: FO(sum), imm: 20 }, // 11 + Jump { target: CO(17), gas: 0 }, // 12 + + // GE2 (13): if r >= 3 goto CASE3 else CASE2 + JumpGreaterEqualU64Imm { target: CO(16), src: FO(r), imm: 3, gas_taken: 0, gas_fallthrough: 0 }, // 13 + // CASE2 (14): sum += 30; goto MERGE + AddU64Imm { dst: FO(sum), src: FO(sum), imm: 30 }, // 14 + Jump { target: CO(17), gas: 0 }, // 15 + + // CASE3 (16): sum += 40 (fallthrough to MERGE) + AddU64Imm { dst: FO(sum), src: FO(sum), imm: 40 }, // 16 + + // MERGE (17): i += 1; goto LOOP + AddU64Imm { dst: FO(i), src: FO(i), imm: 1 }, // 17 + Jump { target: CO(3), gas: 0 }, // 18 + ]; + + let code = arena.alloc_slice_fill_iter(code); + + let func = arena.alloc(Function { + name: GlobalArenaPtr::from_static("match_sum"), + code, + args_size: 8, + args_and_locals_size: args_and_locals_size as usize, + extended_frame_size: args_and_locals_size as usize + + mono_move_core::FRAME_METADATA_SIZE, + zero_frame: false, + frame_layout: FrameLayoutInfo::empty(&arena), + entry_gas: 0, + safe_point_layouts: SortedSafePointEntries::empty(&arena), + }); + + (vec![func], vec![ObjectDescriptor::Trivial], arena) + } +} + +#[cfg(feature = "micro-op")] +pub use micro_op::program as micro_op_match_sum; + +// --------------------------------------------------------------------------- +// Move bytecode +// --------------------------------------------------------------------------- + +#[cfg(feature = "move-bytecode")] +mod move_bytecode { + use move_binary_format::file_format::CompiledModule; + + pub const SOURCE: &str = " +module 0x1::match_sum { + public fun match_sum(n: u64): u64 { + let sum: u64 = 0; + let i: u64 = 0; + while (i < n) { + let r = i % 4; + if (r == 0) { + sum = sum + 10; + } else if (r == 1) { + sum = sum + 20; + } else if (r == 2) { + sum = sum + 30; + } else { + sum = sum + 40; + }; + i = i + 1; + }; + sum + } +} +"; + + pub fn program() -> CompiledModule { + crate::compile_move_source(SOURCE) + } +} + +#[cfg(feature = "move-bytecode")] +pub use move_bytecode::program as move_bytecode_match_sum; diff --git a/third_party/move/mono-move/programs/src/merge_sort.rs b/third_party/move/mono-move/programs/src/merge_sort.rs index 3a5e74d6b76..4303713bfc6 100644 --- a/third_party/move/mono-move/programs/src/merge_sort.rs +++ b/third_party/move/mono-move/programs/src/merge_sort.rs @@ -117,6 +117,7 @@ mod micro_op { extended_frame_size: (callee_hi + 8) as usize, zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(vec), FO(vec_ref)]), + entry_gas: 0, safe_point_layouts: SortedSafePointEntries::empty(&arena), }) }; @@ -152,7 +153,7 @@ mod micro_op { let code = vec![ // if lo + 1 < hi, continue; else return AddU64Imm { dst: FO(tmp), src: FO(lo), imm: 1 }, - JumpLessU64 { target: CO(3), lhs: FO(tmp), rhs: FO(hi) }, + JumpLessU64 { target: CO(3), lhs: FO(tmp), rhs: FO(hi), gas_taken: 0, gas_fallthrough: 0 }, Return, // mid = (lo + hi) / 2 AddU64 { dst: FO(mid), lhs: FO(lo), rhs: FO(hi) }, @@ -185,6 +186,7 @@ mod micro_op { extended_frame_size: (callee_3 + 8) as usize, zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(vec)]), + entry_gas: 0, safe_point_layouts: SortedSafePointEntries::empty(&arena), }) }; @@ -232,51 +234,51 @@ mod micro_op { SlotBorrow { dst: FO(tmp_ref), local: FO(tmp) }, // 4 // MERGE_LOOP (5): both halves have elements? - JumpLessU64 { target: CO(8), lhs: FO(i), rhs: FO(mid) }, // 5 - JumpLessU64 { target: CO(25), lhs: FO(j), rhs: FO(hi) }, // 6: drain right - Jump { target: CO(31) }, // 7: copy back - JumpLessU64 { target: CO(10), lhs: FO(j), rhs: FO(hi) }, // 8 - Jump { target: CO(19) }, // 9: drain left + JumpLessU64 { target: CO(8), lhs: FO(i), rhs: FO(mid), gas_taken: 0, gas_fallthrough: 0 }, // 5 + JumpLessU64 { target: CO(25), lhs: FO(j), rhs: FO(hi), gas_taken: 0, gas_fallthrough: 0 }, // 6: drain right + Jump { target: CO(31), gas: 0 }, // 7: copy back + JumpLessU64 { target: CO(10), lhs: FO(j), rhs: FO(hi), gas_taken: 0, gas_fallthrough: 0 }, // 8 + Jump { target: CO(19), gas: 0 }, // 9: drain left // COMPARE (10): both i and j valid VecLoadElem { dst: FO(elem_a), vec_ref: FO(vec_ref), idx: FO(i), elem_size: 8 }, // 10 VecLoadElem { dst: FO(elem_b), vec_ref: FO(vec_ref), idx: FO(j), elem_size: 8 }, // 11 - JumpLessU64 { target: CO(16), lhs: FO(elem_a), rhs: FO(elem_b) }, // 12 + JumpLessU64 { target: CO(16), lhs: FO(elem_a), rhs: FO(elem_b), gas_taken: 0, gas_fallthrough: 0 }, // 12 // a >= b: push b VecPushBack { vec_ref: FO(tmp_ref), elem: FO(elem_b), elem_size: 8, descriptor_id: DescriptorId(0) }, // 13 AddU64Imm { dst: FO(j), src: FO(j), imm: 1 }, // 14 - Jump { target: CO(5) }, // 15 + Jump { target: CO(5), gas: 0 }, // 15 // PUSH_LEFT (16): a < b, push a VecPushBack { vec_ref: FO(tmp_ref), elem: FO(elem_a), elem_size: 8, descriptor_id: DescriptorId(0) }, // 16 AddU64Imm { dst: FO(i), src: FO(i), imm: 1 }, // 17 - Jump { target: CO(5) }, // 18 + Jump { target: CO(5), gas: 0 }, // 18 // DRAIN_LEFT (19): right exhausted - JumpLessU64 { target: CO(21), lhs: FO(i), rhs: FO(mid) }, // 19 - Jump { target: CO(31) }, // 20 + JumpLessU64 { target: CO(21), lhs: FO(i), rhs: FO(mid), gas_taken: 0, gas_fallthrough: 0 }, // 19 + Jump { target: CO(31), gas: 0 }, // 20 VecLoadElem { dst: FO(elem_a), vec_ref: FO(vec_ref), idx: FO(i), elem_size: 8 }, // 21 VecPushBack { vec_ref: FO(tmp_ref), elem: FO(elem_a), elem_size: 8, descriptor_id: DescriptorId(0) }, // 22 AddU64Imm { dst: FO(i), src: FO(i), imm: 1 }, // 23 - Jump { target: CO(19) }, // 24 + Jump { target: CO(19), gas: 0 }, // 24 // DRAIN_RIGHT (25): left exhausted - JumpLessU64 { target: CO(27), lhs: FO(j), rhs: FO(hi) }, // 25 - Jump { target: CO(31) }, // 26 + JumpLessU64 { target: CO(27), lhs: FO(j), rhs: FO(hi), gas_taken: 0, gas_fallthrough: 0 }, // 25 + Jump { target: CO(31), gas: 0 }, // 26 VecLoadElem { dst: FO(elem_b), vec_ref: FO(vec_ref), idx: FO(j), elem_size: 8 }, // 27 VecPushBack { vec_ref: FO(tmp_ref), elem: FO(elem_b), elem_size: 8, descriptor_id: DescriptorId(0) }, // 28 AddU64Imm { dst: FO(j), src: FO(j), imm: 1 }, // 29 - Jump { target: CO(25) }, // 30 + Jump { target: CO(25), gas: 0 }, // 30 // COPY_BACK (31): copy tmp back into vec[lo..hi) Move8 { dst: FO(k), src: FO(lo) }, // 31 StoreImm8 { dst: FO(tmp_idx), imm: 0 }, // 32 // COPY_LOOP (33) - JumpLessU64 { target: CO(35), lhs: FO(k), rhs: FO(hi) }, // 33 + JumpLessU64 { target: CO(35), lhs: FO(k), rhs: FO(hi), gas_taken: 0, gas_fallthrough: 0 }, // 33 Return, // 34 VecLoadElem { dst: FO(elem_a), vec_ref: FO(tmp_ref), idx: FO(tmp_idx), elem_size: 8 }, // 35 @@ -284,7 +286,7 @@ mod micro_op { src: FO(elem_a), elem_size: 8 }, // 36 AddU64Imm { dst: FO(k), src: FO(k), imm: 1 }, // 37 AddU64Imm { dst: FO(tmp_idx), src: FO(tmp_idx), imm: 1 }, // 38 - Jump { target: CO(33) }, // 39 + Jump { target: CO(33), gas: 0 }, // 39 ]; let code = arena.alloc_slice_fill_iter(code); @@ -301,6 +303,7 @@ mod micro_op { FO(vec_ref), FO(tmp_ref), ]), + entry_gas: 0, safe_point_layouts: SortedSafePointEntries::empty(&arena), }) }; diff --git a/third_party/move/mono-move/programs/src/nested_loop.rs b/third_party/move/mono-move/programs/src/nested_loop.rs index 682a95eed47..97b93700eb4 100644 --- a/third_party/move/mono-move/programs/src/nested_loop.rs +++ b/third_party/move/mono-move/programs/src/nested_loop.rs @@ -85,21 +85,21 @@ mod micro_op { StoreImm8 { dst: FO(sum), imm: 0 }, // 0 StoreImm8 { dst: FO(i), imm: 0 }, // 1 // OUTER_LOOP (2): if i < n goto OUTER_BODY - JumpLessU64 { target: CO(4), lhs: FO(i), rhs: FO(n) }, // 2 - Jump { target: CO(13) }, // 3: goto END + JumpLessU64 { target: CO(4), lhs: FO(i), rhs: FO(n), gas_taken: 0, gas_fallthrough: 0 }, // 2 + Jump { target: CO(13), gas: 0 }, // 3: goto END // OUTER_BODY: j = 0 StoreImm8 { dst: FO(j), imm: 0 }, // 4 // INNER_LOOP (5): if j < n goto INNER_BODY - JumpLessU64 { target: CO(7), lhs: FO(j), rhs: FO(n) }, // 5 - Jump { target: CO(11) }, // 6: goto INNER_END + JumpLessU64 { target: CO(7), lhs: FO(j), rhs: FO(n), gas_taken: 0, gas_fallthrough: 0 }, // 5 + Jump { target: CO(11), gas: 0 }, // 6: goto INNER_END // INNER_BODY: sum += i ^ j; j += 1 XorU64 { dst: FO(tmp), lhs: FO(i), rhs: FO(j) }, // 7 AddU64 { dst: FO(sum), lhs: FO(sum), rhs: FO(tmp) }, // 8 AddU64Imm { dst: FO(j), src: FO(j), imm: 1 }, // 9 - Jump { target: CO(5) }, // 10: goto INNER_LOOP + Jump { target: CO(5), gas: 0 }, // 10: goto INNER_LOOP // INNER_END (11): i += 1; goto OUTER_LOOP AddU64Imm { dst: FO(i), src: FO(i), imm: 1 }, // 11 - Jump { target: CO(2) }, // 12: goto OUTER_LOOP + Jump { target: CO(2), gas: 0 }, // 12: goto OUTER_LOOP // END (13): result = sum Move8 { dst: FO(n), src: FO(sum) }, // 13 Return, // 14 @@ -116,6 +116,7 @@ mod micro_op { + mono_move_core::FRAME_METADATA_SIZE, zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), + entry_gas: 0, safe_point_layouts: SortedSafePointEntries::empty(&arena), }); diff --git a/third_party/move/mono-move/programs/tests/match_sum.rs b/third_party/move/mono-move/programs/tests/match_sum.rs new file mode 100644 index 00000000000..27b322ed609 --- /dev/null +++ b/third_party/move/mono-move/programs/tests/match_sum.rs @@ -0,0 +1,55 @@ +// Copyright (c) Aptos Foundation +// Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE + +use mono_move_programs::match_sum::{native_match_sum, MATCH_SUM_CASES}; + +#[test] +fn native() { + for &(n, expected) in MATCH_SUM_CASES { + assert_eq!(native_match_sum(n), expected, "native_match_sum({n})"); + } +} + +#[cfg(feature = "micro-op")] +mod micro_op { + use mono_move_gas::SimpleGasMeter; + use mono_move_programs::match_sum::{micro_op_match_sum, MATCH_SUM_CASES}; + use mono_move_runtime::InterpreterContext; + + fn run(n: u64) -> u64 { + let (functions, descriptors, _arena) = micro_op_match_sum(); + let gas_meter = SimpleGasMeter::new(u64::MAX); + let mut ctx = InterpreterContext::new(&descriptors, gas_meter, unsafe { + functions[0].as_ref_unchecked() + }); + ctx.set_root_arg(0, &n.to_le_bytes()); + ctx.run().unwrap(); + ctx.root_result() + } + + #[test] + fn correctness() { + for &(n, expected) in MATCH_SUM_CASES { + assert_eq!(run(n), expected, "micro_op match_sum({n})"); + } + } +} + +#[cfg(feature = "move-bytecode")] +mod move_bytecode { + use super::MATCH_SUM_CASES; + use mono_move_programs::{match_sum::move_bytecode_match_sum, testing}; + + fn run(n: u64) -> u64 { + let module = move_bytecode_match_sum(); + let result = testing::run_move_function(&module, "match_sum", vec![testing::arg_u64(n)]); + testing::return_u64(&result) + } + + #[test] + fn correctness() { + for &(n, expected) in MATCH_SUM_CASES { + assert_eq!(run(n), expected, "move_bytecode match_sum({n})"); + } + } +} diff --git a/third_party/move/mono-move/runtime/src/interpreter.rs b/third_party/move/mono-move/runtime/src/interpreter.rs index 6ac9fa75ebe..42e741a6ed8 100644 --- a/third_party/move/mono-move/runtime/src/interpreter.rs +++ b/third_party/move/mono-move/runtime/src/interpreter.rs @@ -217,61 +217,109 @@ impl InterpreterContext<'_, G> { return self.call(func, fp, ptr.as_ref_unchecked()); }, - MicroOp::JumpNotZeroU64 { target, src } => { - self.pc = if read_u64(fp, src) != 0 { - target.into() + MicroOp::JumpNotZeroU64 { + target, + src, + gas_taken, + gas_fallthrough, + } => { + if read_u64(fp, src) != 0 { + self.gas_meter.charge(gas_taken as u64)?; + self.pc = target.into(); } else { - self.pc + 1 - }; + self.gas_meter.charge(gas_fallthrough as u64)?; + self.pc = self.pc + 1; + } return Ok(StepResult::Continue); }, - MicroOp::JumpGreaterEqualU64Imm { target, src, imm } => { - self.pc = if read_u64(fp, src) >= imm { - target.into() + MicroOp::JumpGreaterEqualU64Imm { + target, + src, + imm, + gas_taken, + gas_fallthrough, + } => { + if read_u64(fp, src) >= imm { + self.gas_meter.charge(gas_taken as u64)?; + self.pc = target.into(); } else { - self.pc + 1 - }; + self.gas_meter.charge(gas_fallthrough as u64)?; + self.pc = self.pc + 1; + } return Ok(StepResult::Continue); }, - MicroOp::JumpLessU64Imm { target, src, imm } => { - self.pc = if read_u64(fp, src) < imm { - target.into() + MicroOp::JumpLessU64Imm { + target, + src, + imm, + gas_taken, + gas_fallthrough, + } => { + if read_u64(fp, src) < imm { + self.gas_meter.charge(gas_taken as u64)?; + self.pc = target.into(); } else { - self.pc + 1 - }; + self.gas_meter.charge(gas_fallthrough as u64)?; + self.pc = self.pc + 1; + } return Ok(StepResult::Continue); }, - MicroOp::JumpLessU64 { target, lhs, rhs } => { - self.pc = if read_u64(fp, lhs) < read_u64(fp, rhs) { - target.into() + MicroOp::JumpLessU64 { + target, + lhs, + rhs, + gas_taken, + gas_fallthrough, + } => { + if read_u64(fp, lhs) < read_u64(fp, rhs) { + self.gas_meter.charge(gas_taken as u64)?; + self.pc = target.into(); } else { - self.pc + 1 - }; + self.gas_meter.charge(gas_fallthrough as u64)?; + self.pc = self.pc + 1; + } return Ok(StepResult::Continue); }, - MicroOp::JumpGreaterEqualU64 { target, lhs, rhs } => { - self.pc = if read_u64(fp, lhs) >= read_u64(fp, rhs) { - target.into() + MicroOp::JumpGreaterEqualU64 { + target, + lhs, + rhs, + gas_taken, + gas_fallthrough, + } => { + if read_u64(fp, lhs) >= read_u64(fp, rhs) { + self.gas_meter.charge(gas_taken as u64)?; + self.pc = target.into(); } else { - self.pc + 1 - }; + self.gas_meter.charge(gas_fallthrough as u64)?; + self.pc = self.pc + 1; + } return Ok(StepResult::Continue); }, - MicroOp::JumpNotEqualU64 { target, lhs, rhs } => { - self.pc = if read_u64(fp, lhs) != read_u64(fp, rhs) { - target.into() + MicroOp::JumpNotEqualU64 { + target, + lhs, + rhs, + gas_taken, + gas_fallthrough, + } => { + if read_u64(fp, lhs) != read_u64(fp, rhs) { + self.gas_meter.charge(gas_taken as u64)?; + self.pc = target.into(); } else { - self.pc + 1 - }; + self.gas_meter.charge(gas_fallthrough as u64)?; + self.pc = self.pc + 1; + } return Ok(StepResult::Continue); }, - MicroOp::Jump { target } => { + MicroOp::Jump { target, gas } => { + self.gas_meter.charge(gas)?; self.pc = target.into(); return Ok(StepResult::Continue); }, @@ -626,10 +674,6 @@ impl InterpreterContext<'_, G> { write_ptr(fp, dst, obj_ptr); write_u64(fp, dst + 8, offset as u64); }, - - MicroOp::Charge { cost } => { - self.gas_meter.charge(cost)?; - }, } } @@ -650,6 +694,8 @@ impl InterpreterContext<'_, G> { fp: *mut u8, callee: &Function, ) -> ExecutionResult { + // Charge the callee's entry block before any of its instructions execute. + self.gas_meter.charge(callee.entry_gas)?; unsafe { let new_fp = fp.add(caller.args_and_locals_size + FRAME_METADATA_SIZE); let stack_end = self.stack.as_ptr().add(self.stack.len()); @@ -685,6 +731,11 @@ impl InterpreterContext<'_, G> { // (VecPushBack, etc.) take &mut self, which may alias these fields. // Write back only on CallFunc/Return. pub fn run(&mut self) -> ExecutionResult<()> { + // Charge the entry block of the function being executed before any + // instructions run. For functions entered via CallLocalFunc this is + // handled inside call(); here we handle the root invocation. + let entry_gas = unsafe { self.current_func.as_ref() }.entry_gas; + self.gas_meter.charge(entry_gas)?; loop { match self.step()? { StepResult::Continue => {}, diff --git a/third_party/move/mono-move/runtime/src/verifier.rs b/third_party/move/mono-move/runtime/src/verifier.rs index 12aec061220..cef64bf6092 100644 --- a/third_party/move/mono-move/runtime/src/verifier.rs +++ b/third_party/move/mono-move/runtime/src/verifier.rs @@ -216,36 +216,34 @@ impl FunctionVerifier<'_> { self.check_frame_access(Some(pc), dst, size); }, - MicroOp::Jump { target } => { + MicroOp::Jump { target, .. } => { self.check_jump(pc, target); }, - MicroOp::JumpNotZeroU64 { target, src } => { + MicroOp::JumpNotZeroU64 { target, src, .. } => { self.check_frame_access_8(pc, src); self.check_jump(pc, target); }, - MicroOp::JumpGreaterEqualU64Imm { - target, - src, - imm: _, - } => { + MicroOp::JumpGreaterEqualU64Imm { target, src, .. } => { self.check_frame_access_8(pc, src); self.check_jump(pc, target); }, - MicroOp::JumpLessU64Imm { - target, - src, - imm: _, - } => { + MicroOp::JumpLessU64Imm { target, src, .. } => { self.check_frame_access_8(pc, src); self.check_jump(pc, target); }, - MicroOp::JumpLessU64 { target, lhs, rhs } - | MicroOp::JumpGreaterEqualU64 { target, lhs, rhs } - | MicroOp::JumpNotEqualU64 { target, lhs, rhs } => { + MicroOp::JumpLessU64 { + target, lhs, rhs, .. + } + | MicroOp::JumpGreaterEqualU64 { + target, lhs, rhs, .. + } + | MicroOp::JumpNotEqualU64 { + target, lhs, rhs, .. + } => { self.check_frame_access_8(pc, lhs); self.check_frame_access_8(pc, rhs); self.check_jump(pc, target); @@ -405,9 +403,6 @@ impl FunctionVerifier<'_> { self.check_nonzero_size(pc, size); self.check_frame_access(Some(pc), src, size); }, - - // Inserted by the instrumentation pass; no frame accesses to verify. - MicroOp::Charge { .. } => {}, } } diff --git a/third_party/move/mono-move/runtime/tests/enum_test.rs b/third_party/move/mono-move/runtime/tests/enum_test.rs index 2ab6084ac3d..6c27b1e8fba 100644 --- a/third_party/move/mono-move/runtime/tests/enum_test.rs +++ b/third_party/move/mono-move/runtime/tests/enum_test.rs @@ -51,6 +51,7 @@ fn enum_basic() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(shape)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Enum { size: 24, @@ -108,6 +109,7 @@ fn enum_survives_gc() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(shape)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Enum { size: 24, @@ -172,6 +174,7 @@ fn enum_gc_traces_refs() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(val), FO(vec), FO(vec_ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ ObjectDescriptor::Enum { @@ -226,7 +229,7 @@ fn enum_pattern_match() { StoreImm8 { dst: FO(tmp), imm: 25 }, MicroOp::enum_store8(FO(op), 8, FO(tmp)), MicroOp::enum_get_tag(FO(op), FO(tmp)), - JumpNotZeroU64 { target: CO(12), src: FO(tmp) }, + JumpNotZeroU64 { target: CO(12), src: FO(tmp), gas_taken: 0, gas_fallthrough: 0 }, MicroOp::enum_load8(FO(op), 0, FO(result)), MicroOp::enum_load8(FO(op), 8, FO(tmp)), AddU64 { dst: FO(result), lhs: FO(result), rhs: FO(tmp) }, @@ -243,6 +246,7 @@ fn enum_pattern_match() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(op)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Enum { size: 24, @@ -295,6 +299,7 @@ fn enum_variant_switch() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(e)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Enum { size: 16, @@ -349,6 +354,7 @@ fn enum_borrow_field() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(e), FO(r#ref), FO(e_ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Enum { size: 24, @@ -408,6 +414,7 @@ fn enum_gc_variant_switching() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(ctr), FO(vec), FO(vec_ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ ObjectDescriptor::Enum { @@ -472,6 +479,7 @@ fn enum_in_struct() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(wrapper), FO(payload)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ ObjectDescriptor::Struct { @@ -550,6 +558,7 @@ fn enum_in_vector() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(vec), FO(e), FO(vec_ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ ObjectDescriptor::Enum { diff --git a/third_party/move/mono-move/runtime/tests/gc_stress.rs b/third_party/move/mono-move/runtime/tests/gc_stress.rs index 9300e7dc135..31d14988884 100644 --- a/third_party/move/mono-move/runtime/tests/gc_stress.rs +++ b/third_party/move/mono-move/runtime/tests/gc_stress.rs @@ -139,6 +139,7 @@ fn make_gc_stress_program( FO(callee_vec_ref), ]), safe_point_layouts: SortedSafePointEntries::empty(arena), + entry_gas: 0, }); // -- Function 0: main -- @@ -165,16 +166,16 @@ fn make_gc_stress_program( StoreImm8 { dst: FO(const_hundred), imm: 100 }, // ---- LOOP (PC 4) ---- - JumpGreaterEqualU64Imm { target: CO(30), src: FO(i), imm: num_iterations }, + JumpGreaterEqualU64Imm { target: CO(30), src: FO(i), imm: num_iterations, gas_taken: 0, gas_fallthrough: 0 }, // PC 5: r1 = random (action) StoreRandomU64 { dst: FO(r1) }, // PC 6: tmp = r1 % 100 ModU64 { dst: FO(tmp), lhs: FO(r1), rhs: FO(const_hundred) }, // PC 7: if tmp >= 45: goto GARBAGE (PC 25) - JumpGreaterEqualU64Imm { target: CO(25), src: FO(tmp), imm: 45 }, + JumpGreaterEqualU64Imm { target: CO(25), src: FO(tmp), imm: 45, gas_taken: 0, gas_fallthrough: 0 }, // PC 8: if tmp >= 30: goto POP (PC 20) - JumpGreaterEqualU64Imm { target: CO(20), src: FO(tmp), imm: 30 }, + JumpGreaterEqualU64Imm { target: CO(20), src: FO(tmp), imm: 30, gas_taken: 0, gas_fallthrough: 0 }, // ---- PUSH_OR_REPLACE (action 0..29) ---- // PC 9: r1 = random (val) @@ -187,11 +188,11 @@ fn make_gc_stress_program( // PC 12: len = VecLen(outer_vec_ref) VecLen { dst: FO(len), vec_ref: FO(outer_vec_ref) }, // PC 13: if len >= max_len: goto REPLACE (PC 16) - JumpGreaterEqualU64Imm { target: CO(16), src: FO(len), imm: max_len }, + JumpGreaterEqualU64Imm { target: CO(16), src: FO(len), imm: max_len, gas_taken: 0, gas_fallthrough: 0 }, // ---- PUSH (PC 14) ---- VecPushBack { vec_ref: FO(outer_vec_ref), elem: FO(entry_ptr), elem_size: 8, descriptor_id: DescriptorId(2) }, // PC 15: goto NEXT (PC 28) - Jump { target: CO(28) }, + Jump { target: CO(28), gas: 0 }, // ---- REPLACE (PC 16) ---- // PC 16: r1 = random (idx_raw) @@ -201,19 +202,19 @@ fn make_gc_stress_program( // PC 18: outer_vec[tmp] = entry_ptr VecStoreElem { vec_ref: FO(outer_vec_ref), idx: FO(tmp), src: FO(entry_ptr), elem_size: 8 }, // PC 19: goto NEXT (PC 28) - Jump { target: CO(28) }, + Jump { target: CO(28), gas: 0 }, // ---- POP (PC 20) ---- // PC 20: len = VecLen(outer_vec_ref) VecLen { dst: FO(len), vec_ref: FO(outer_vec_ref) }, // PC 21: if len > 0: goto DO_POP (PC 23) - JumpNotZeroU64 { target: CO(23), src: FO(len) }, + JumpNotZeroU64 { target: CO(23), src: FO(len), gas_taken: 0, gas_fallthrough: 0 }, // PC 22: len == 0, skip → goto NEXT (PC 28) - Jump { target: CO(28) }, + Jump { target: CO(28), gas: 0 }, // PC 23: VecPopBack(outer_vec_ref) — discard into r1 VecPopBack { dst: FO(r1), vec_ref: FO(outer_vec_ref), elem_size: 8 }, // PC 24: goto NEXT (PC 28) - Jump { target: CO(28) }, + Jump { target: CO(28), gas: 0 }, // ---- GARBAGE (PC 25) ---- // PC 25: r1 = random (val) @@ -227,7 +228,7 @@ fn make_gc_stress_program( // ---- NEXT (PC 28) ---- AddU64Imm { dst: FO(i), src: FO(i), imm: 1 }, // PC 29: goto LOOP (PC 4) - Jump { target: CO(4) }, + Jump { target: CO(4), gas: 0 }, // ---- DONE (PC 30) ---- Return, @@ -245,6 +246,7 @@ fn make_gc_stress_program( FO(entry_ptr), ]), safe_point_layouts: SortedSafePointEntries::empty(arena), + entry_gas: 0, }); let descriptors = vec![ diff --git a/third_party/move/mono-move/runtime/tests/ref_test.rs b/third_party/move/mono-move/runtime/tests/ref_test.rs index 404000aae36..8015f418a5c 100644 --- a/third_party/move/mono-move/runtime/tests/ref_test.rs +++ b/third_party/move/mono-move/runtime/tests/ref_test.rs @@ -59,6 +59,7 @@ fn ref_basic() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(vec), FO(r#ref), FO(vec_ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Trivial]; let gas_meter = SimpleGasMeter::new(u64::MAX); @@ -123,6 +124,7 @@ fn ref_survives_gc() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(vec), FO(ref_base), FO(vec_ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Trivial]; let gas_meter = SimpleGasMeter::new(u64::MAX); @@ -176,6 +178,7 @@ fn ref_cross_frame() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(c_ref_base)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }); // -- Function 0: main -- @@ -216,6 +219,7 @@ fn ref_cross_frame() { FO(m_callee_ref), ]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }); let descriptors = vec![ObjectDescriptor::Trivial]; @@ -302,6 +306,7 @@ fn ref_multiple_borrows() { FO(vec_ref), ]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Trivial]; let gas_meter = SimpleGasMeter::new(u64::MAX); @@ -369,6 +374,7 @@ fn ref_borrow_local() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(ref_base), FO(vec), FO(vec_ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Trivial]; let gas_meter = SimpleGasMeter::new(u64::MAX); @@ -456,6 +462,7 @@ fn ref_nested_vectors() { FO(inner_ref), ]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Trivial, ObjectDescriptor::Vector { elem_size: 8, @@ -544,6 +551,7 @@ fn ref_survives_double_gc() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(vec), FO(ref_base), FO(vec_ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Trivial]; let gas_meter = SimpleGasMeter::new(u64::MAX); @@ -605,6 +613,7 @@ fn ref_struct_field_borrow() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(entry), FO(r#ref), FO(entry_ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Struct { size: 16, @@ -664,6 +673,7 @@ fn ref_struct_field_survives_gc() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(entry), FO(ref_base), FO(entry_ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Struct { size: 16, diff --git a/third_party/move/mono-move/runtime/tests/struct_test.rs b/third_party/move/mono-move/runtime/tests/struct_test.rs index 05b21aea91f..5175b9d8ca9 100644 --- a/third_party/move/mono-move/runtime/tests/struct_test.rs +++ b/third_party/move/mono-move/runtime/tests/struct_test.rs @@ -43,6 +43,7 @@ fn struct_inline() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Trivial]; let gas_meter = SimpleGasMeter::new(u64::MAX); @@ -89,6 +90,7 @@ fn struct_inline_borrow() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(r#ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Trivial]; let gas_meter = SimpleGasMeter::new(u64::MAX); @@ -135,6 +137,7 @@ fn struct_heap_basic() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(entry)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Struct { size: 16, @@ -185,6 +188,7 @@ fn struct_heap_survives_gc() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(entry)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Struct { size: 16, @@ -256,6 +260,7 @@ fn struct_with_vector_field() { FO(ctr_ref), ]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ ObjectDescriptor::Struct { @@ -324,6 +329,7 @@ fn struct_borrow_field() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(entry), FO(r#ref), FO(entry_ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Struct { size: 16, @@ -380,6 +386,7 @@ fn struct_borrow_survives_gc() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(entry), FO(ref_base), FO(entry_ref)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, })]; let descriptors = vec![ObjectDescriptor::Struct { size: 16, diff --git a/third_party/move/mono-move/runtime/tests/vec_sum.rs b/third_party/move/mono-move/runtime/tests/vec_sum.rs index 0beadda3cef..416a78d5007 100644 --- a/third_party/move/mono-move/runtime/tests/vec_sum.rs +++ b/third_party/move/mono-move/runtime/tests/vec_sum.rs @@ -32,20 +32,20 @@ fn make_vec_sum_program( VecNew { dst: FO(slot_vec) }, SlotBorrow { dst: FO(slot_vec_ref), local: FO(slot_vec) }, StoreImm8 { dst: FO(slot_i), imm: 0 }, - JumpGreaterEqualU64Imm { target: CO(9), src: FO(slot_i), imm: n }, + JumpGreaterEqualU64Imm { target: CO(9), src: FO(slot_i), imm: n, gas_taken: 0, gas_fallthrough: 0 }, VecPushBack { vec_ref: FO(slot_vec_ref), elem: FO(slot_i), elem_size: 8, descriptor_id: DescriptorId(0) }, StoreImm8 { dst: FO(slot_tmp), imm: 1 }, AddU64 { dst: FO(slot_i), lhs: FO(slot_i), rhs: FO(slot_tmp) }, - JumpGreaterEqualU64Imm { target: CO(9), src: FO(slot_i), imm: n }, - JumpNotZeroU64 { target: CO(4), src: FO(slot_i) }, + JumpGreaterEqualU64Imm { target: CO(9), src: FO(slot_i), imm: n, gas_taken: 0, gas_fallthrough: 0 }, + JumpNotZeroU64 { target: CO(4), src: FO(slot_i), gas_taken: 0, gas_fallthrough: 0 }, StoreImm8 { dst: FO(slot_result), imm: 0 }, VecLen { dst: FO(slot_i), vec_ref: FO(slot_vec_ref) }, - JumpNotZeroU64 { target: CO(13), src: FO(slot_i) }, + JumpNotZeroU64 { target: CO(13), src: FO(slot_i), gas_taken: 0, gas_fallthrough: 0 }, Return, VecPopBack { dst: FO(slot_tmp), vec_ref: FO(slot_vec_ref), elem_size: 8 }, AddU64 { dst: FO(slot_result), lhs: FO(slot_result), rhs: FO(slot_tmp) }, VecLen { dst: FO(slot_i), vec_ref: FO(slot_vec_ref) }, - JumpNotZeroU64 { target: CO(13), src: FO(slot_i) }, + JumpNotZeroU64 { target: CO(13), src: FO(slot_i), gas_taken: 0, gas_fallthrough: 0 }, Return, ]); let func = arena.alloc(Function { @@ -57,6 +57,7 @@ fn make_vec_sum_program( zero_frame: true, frame_layout: FrameLayoutInfo::new(arena, vec![FO(slot_vec), FO(slot_vec_ref)]), safe_point_layouts: SortedSafePointEntries::empty(arena), + entry_gas: 0, }); let descriptors = vec![ObjectDescriptor::Trivial]; diff --git a/third_party/move/mono-move/runtime/tests/verifier_test.rs b/third_party/move/mono-move/runtime/tests/verifier_test.rs index 70967e1c739..269cbfce730 100644 --- a/third_party/move/mono-move/runtime/tests/verifier_test.rs +++ b/third_party/move/mono-move/runtime/tests/verifier_test.rs @@ -28,6 +28,7 @@ fn minimal_func(arena: &ExecutableArena) -> &Function { zero_frame: false, frame_layout: FrameLayoutInfo::empty(arena), safe_point_layouts: SortedSafePointEntries::empty(arena), + entry_gas: 0, }) .as_ref_unchecked() } @@ -55,7 +56,7 @@ fn valid_with_arithmetic_and_jumps() { StoreImm8 { dst: FO(0), imm: 10 }, StoreImm8 { dst: FO(8), imm: 1 }, SubU64Imm { dst: FO(0), src: FO(0), imm: 1 }, - JumpNotZeroU64 { target: CO(2), src: FO(0) }, + JumpNotZeroU64 { target: CO(2), src: FO(0), gas_taken: 0, gas_fallthrough: 0 }, Return, ]); // SAFETY: Arena is alive for the duration of the test. @@ -70,6 +71,7 @@ fn valid_with_arithmetic_and_jumps() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -103,6 +105,7 @@ fn valid_with_vec_and_pointer_slots() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(0)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -130,6 +133,7 @@ fn frame_bounds_store_u64() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -165,6 +169,7 @@ fn frame_bounds_mov() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -198,6 +203,7 @@ fn frame_bounds_fat_ptr_write() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -224,6 +230,7 @@ fn frame_bounds_callfunc_metadata() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -253,6 +260,7 @@ fn pointer_slots_offset_out_of_bounds() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(100)]), // offset 100 + 8 > extended_frame_size 32 safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -278,6 +286,7 @@ fn pointer_slots_overlaps_metadata() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(8)]), // offset 8 overlaps metadata [8, 32) since args_and_locals_size = 8 safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -303,6 +312,7 @@ fn args_size_exceeds_data_size() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -325,7 +335,10 @@ fn invalid_jump_target() { .alloc(Function { name: GlobalArenaPtr::from_static("test"), code: arena.alloc_slice_fill_iter(vec![ - Jump { target: CO(5) }, // only 2 instructions -> 5 >= 2 + Jump { + target: CO(5), + gas: 0, + }, // only 2 instructions -> 5 >= 2 Return, ]), args_size: 0, @@ -334,6 +347,7 @@ fn invalid_jump_target() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -356,6 +370,8 @@ fn invalid_conditional_jump_target() { JumpNotZeroU64 { target: CO(99), src: FO(0), + gas_taken: 0, + gas_fallthrough: 0, }, Return, ]), @@ -365,6 +381,7 @@ fn invalid_conditional_jump_target() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -394,6 +411,7 @@ fn invalid_callfunc_func_id() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -440,6 +458,7 @@ fn invalid_descriptor_id() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(0)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -475,6 +494,7 @@ fn zero_size_mov() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -517,6 +537,7 @@ fn zero_elem_size_vec_push() { zero_frame: true, frame_layout: FrameLayoutInfo::new(&arena, vec![FO(0)]), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -544,6 +565,7 @@ fn empty_code() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -567,6 +589,7 @@ fn zero_frame_size() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; @@ -593,7 +616,10 @@ fn multiple_errors_collected() { dst: FO(100), imm: 0, }, // out of bounds - Jump { target: CO(99) }, // invalid target + Jump { + target: CO(99), + gas: 0, + }, // invalid target Return, ]), args_size: 0, @@ -602,6 +628,7 @@ fn multiple_errors_collected() { zero_frame: false, frame_layout: FrameLayoutInfo::empty(&arena), safe_point_layouts: SortedSafePointEntries::empty(&arena), + entry_gas: 0, }) .as_ref_unchecked() }; diff --git a/third_party/move/mono-move/specializer/src/lower/translate.rs b/third_party/move/mono-move/specializer/src/lower/translate.rs index c1489c94fbb..6db4c75cdbe 100644 --- a/third_party/move/mono-move/specializer/src/lower/translate.rs +++ b/third_party/move/mono-move/specializer/src/lower/translate.rs @@ -288,6 +288,7 @@ impl<'a> LoweringState<'a> { self.branch_fixups.push(idx); self.emit(MicroOp::Jump { target: CodeOffset(encode_label(*l)), + gas: 0, }); }, Instr::BrTrue(Label(l), cond) => { @@ -297,6 +298,8 @@ impl<'a> LoweringState<'a> { self.emit(MicroOp::JumpNotZeroU64 { target: CodeOffset(encode_label(*l)), src: FrameOffset(s.offset), + gas_taken: 0, + gas_fallthrough: 0, }); }, Instr::BrFalse(Label(_l), _cond) => { @@ -396,6 +399,8 @@ impl<'a> LoweringState<'a> { target: CodeOffset(encode_label(*l)), lhs: FrameOffset(l_slot.offset), rhs: FrameOffset(r_slot.offset), + gas_taken: 0, + gas_fallthrough: 0, }); Some(true) }, @@ -424,6 +429,8 @@ impl<'a> LoweringState<'a> { target: CodeOffset(encode_label(*l)), src: FrameOffset(s.offset), imm: v + 1, + gas_taken: 0, + gas_fallthrough: 0, }); Some(true) }, @@ -437,6 +444,8 @@ impl<'a> LoweringState<'a> { target: CodeOffset(encode_label(*l)), src: FrameOffset(s.offset), imm: v, + gas_taken: 0, + gas_fallthrough: 0, }); Some(true) }, @@ -448,7 +457,7 @@ impl<'a> LoweringState<'a> { for &idx in &self.branch_fixups { // Extract the encoded label from the op, resolve it, then patch. let encoded = match &self.ops[idx] { - MicroOp::Jump { target } => target.0, + MicroOp::Jump { target, .. } => target.0, MicroOp::JumpNotZeroU64 { target, .. } => target.0, MicroOp::JumpGreaterEqualU64Imm { target, .. } => target.0, MicroOp::JumpLessU64 { target, .. } => target.0, @@ -461,7 +470,7 @@ impl<'a> LoweringState<'a> { let label = decode_label(encoded); let resolved = self.resolve_label(label)?; match &mut self.ops[idx] { - MicroOp::Jump { target } => target.0 = resolved, + MicroOp::Jump { target, .. } => target.0 = resolved, MicroOp::JumpNotZeroU64 { target, .. } => target.0 = resolved, MicroOp::JumpGreaterEqualU64Imm { target, .. } => target.0 = resolved, MicroOp::JumpLessU64 { target, .. } => target.0 = resolved,