Skip to content

Commit a463e31

Browse files
committed
perf: optimize constant-base exp
1 parent 03a9a10 commit a463e31

5 files changed

Lines changed: 197 additions & 65 deletions

File tree

crates/revmc-builtins/src/ir.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ macro_rules! builtins {
166166
const PANIC: u8 = _0_0;
167167
const ASSERTSPECID: u8 = _0_0;
168168

169+
const EXPGAS: u8 = _1_0;
169170
const KECCAK256CC: u8 = _0_1;
170171

171172
const CALLDATALOADC: u8 = _0_1;
@@ -256,6 +257,7 @@ builtins! {
256257
AddMod = __revmc_builtin_addmod(@[sp] ptr) None,
257258
MulMod = __revmc_builtin_mulmod(@[sp] ptr) None,
258259
Exp = __revmc_builtin_exp(@[ecx] ptr, @[sp] ptr) None,
260+
ExpGas = __revmc_builtin_exp_gas(@[ecx] ptr, @[sp] ptr) None,
259261
Keccak256 = __revmc_builtin_keccak256(@[ecx] ptr, @[sp] ptr) None,
260262
Keccak256CC = __revmc_builtin_keccak256_cc(@[ecx] ptr, @[sp] ptr, usize, usize) None,
261263
Balance = __revmc_builtin_balance(@[ecx] ptr, @[sp] ptr) None,

crates/revmc-builtins/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,15 @@ pub unsafe extern "C" fn __revmc_builtin_exp(
167167
Ok(())
168168
}
169169

170+
#[unsafe(no_mangle)]
171+
pub unsafe extern "C" fn __revmc_builtin_exp_gas(
172+
ecx: &mut EvmContext<'_>,
173+
exponent: &EvmWord,
174+
) -> BuiltinResult {
175+
gas!(ecx, ecx.gas_params.exp_cost(exponent.to_u256()));
176+
Ok(())
177+
}
178+
170179
#[unsafe(no_mangle)]
171180
pub unsafe extern "C" fn __revmc_builtin_keccak256(
172181
ecx: &mut EvmContext<'_>,

crates/revmc-codegen/src/compiler/translate/peephole.rs

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Peephole optimizations applied during translation.
22
//!
33
//! These fire when abstract interpretation has proven one or more operands are constant,
4-
//! replacing expensive opaque builtins (DIV, MOD, SDIV, SMOD, ADDMOD, MULMOD) with native
4+
//! replacing expensive opaque builtins (DIV, MOD, SDIV, SMOD, ADDMOD, MULMOD, EXP) with native
55
//! LLVM operations that it can optimize further (e.g. pow2 udiv → lshr, pow2 urem → and).
66
77
use super::FunctionCx;
@@ -178,8 +178,10 @@ impl<'a, B: Backend> FunctionCx<'a, B> {
178178
///
179179
/// Dynamic gas is folded into the section gas cost by `SectionsAnalysis` when the
180180
/// exponent is a compile-time constant, so the section gas check already covers it.
181+
/// For constant-base cases with a dynamic exponent, call `ExpGas` to charge only the
182+
/// dynamic gas and compute the result inline.
181183
fn peephole_exp(&mut self) -> bool {
182-
let [_base, exponent] = self.const_operands();
184+
let [base, exponent] = self.const_operands();
183185
match exponent {
184186
// x ** 0 => 1.
185187
Some(U256::ZERO) => self.fold_const(1),
@@ -194,11 +196,63 @@ impl<'a, B: Backend> FunctionCx<'a, B> {
194196
let r = self.bcx.imul(a, a);
195197
self.push(r);
196198
}
199+
Some(_) => return false,
200+
None => {}
201+
}
202+
if exponent.is_some() {
203+
return true;
204+
}
205+
match base {
206+
// 0 ** x => x == 0 ? 1 : 0.
207+
Some(U256::ZERO) => {
208+
self.pay_exp_dynamic_gas();
209+
let [_, exponent] = self.popn();
210+
let is_zero = self.bcx.icmp_imm(IntCC::Equal, exponent, 0);
211+
let one = self.bcx.iconst_256(1);
212+
let zero = self.bcx.iconst_256(0);
213+
let r = self.bcx.select(is_zero, one, zero);
214+
self.push(r);
215+
}
216+
// 1 ** x => 1.
217+
Some(U256::ONE) => {
218+
self.pay_exp_dynamic_gas();
219+
self.fold_const(1);
220+
}
221+
// (-1) ** x => x % 2 == 0 ? 1 : -1.
222+
Some(U256::MAX) => {
223+
self.pay_exp_dynamic_gas();
224+
let [_, exponent] = self.popn();
225+
let is_odd = self.bcx.bitand_imm(exponent, 1);
226+
let is_even = self.bcx.icmp_imm(IntCC::Equal, is_odd, 0);
227+
let one = self.bcx.iconst_256(1);
228+
let minus_one = self.bcx.iconst_256(U256::MAX);
229+
let r = self.bcx.select(is_even, one, minus_one);
230+
self.push(r);
231+
}
232+
// (2 ** k) ** x => x < ceil(256 / k) ? 1 << (k * x) : 0.
233+
Some(base) if base.is_power_of_two() => {
234+
self.pay_exp_dynamic_gas();
235+
let k = base.trailing_zeros();
236+
let threshold = i64::from(256_u16.div_ceil(k as u16));
237+
let [_, exponent] = self.popn();
238+
let in_range = self.bcx.icmp_imm(IntCC::UnsignedLessThan, exponent, threshold);
239+
let shift = if k == 1 { exponent } else { self.bcx.imul_imm(exponent, k as i64) };
240+
let one = self.bcx.iconst_256(1);
241+
let shifted = self.bcx.ishl(one, shift);
242+
let zero = self.bcx.iconst_256(0);
243+
let r = self.bcx.select(in_range, shifted, zero);
244+
self.push(r);
245+
}
197246
_ => return false,
198247
}
199248
true
200249
}
201250

251+
fn pay_exp_dynamic_gas(&mut self) {
252+
let exponent = self.sp_after_inputs_with(&[1]);
253+
self.call_fallible_builtin(Builtin::ExpGas, &[self.ecx, exponent]);
254+
}
255+
202256
/// SIGNEXTEND ext, x => sign-extend x from (ext+1) bytes.
203257
fn peephole_signextend(&mut self) -> bool {
204258
let [ext, _x] = self.const_operands();
@@ -319,10 +373,7 @@ impl<'a, B: Backend> FunctionCx<'a, B> {
319373
fn const_memory_operands(&self, args: [Option<U256>; 2]) -> Option<(u64, u64)> {
320374
if let [offset, Some(len)] = args
321375
&& let Ok(len) = u64::try_from(len)
322-
// For len == 0 offset is ignored.
323-
&& let Some(offset) = offset
324-
.and_then(|offset| u64::try_from(offset).ok())
325-
.or_else(|| (len == 0).then_some(0))
376+
&& let Some(offset) = offset.and_then(|offset| u64::try_from(offset).ok())
326377
{
327378
Some((offset, len))
328379
} else {

crates/revmc-codegen/src/tests/macros.rs

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,12 @@ macro_rules! tests {
8181
)*
8282
};
8383

84-
// Generate an `_opaque` companion test for each arity.
85-
// Uses MSTORE+MLOAD to make operands invisible to the compiler.
84+
// Generate companion tests for every const/dynamic operand combination.
85+
// Dynamic operands use MSTORE+MLOAD to make them invisible to the compiler.
8686
(@maybe_opaque $name:ident(@raw { $($fields:tt)* })) => {};
8787
(@maybe_opaque $name:ident($op:expr, $a:expr => $($ret:expr),* $(; $($_r1:tt)*)?)) => {
8888
paste::paste! {
89-
matrix_tests!([<$name _opaque>] = |jit| run_test_case(
89+
matrix_tests!([<$name _dyn>] = |jit| run_test_case(
9090
&TestCase {
9191
bytecode: &bytecode_unop_opaque($op, $a),
9292
expected_stack: &[$($ret),*],
@@ -100,33 +100,49 @@ macro_rules! tests {
100100
};
101101
(@maybe_opaque $name:ident($op:expr, $a:expr, $b:expr => $($ret:expr),* $(; $($_r2:tt)*)?)) => {
102102
paste::paste! {
103-
matrix_tests!([<$name _opaque>] = |jit| run_test_case(
104-
&TestCase {
105-
bytecode: &bytecode_binop_opaque($op, $a, $b),
106-
expected_stack: &[$($ret),*],
107-
expected_memory: MEMORY_WHAT_INTERPRETER_SAYS,
108-
expected_gas: GAS_WHAT_INTERPRETER_SAYS,
109-
..Default::default()
110-
},
111-
jit,
112-
));
103+
tests!(@mixed_binop [<$name _const_dyn>] $op, $a, $b, true, false => $($ret),*);
104+
tests!(@mixed_binop [<$name _dyn_const>] $op, $a, $b, false, true => $($ret),*);
105+
tests!(@mixed_binop [<$name _dyn_dyn>] $op, $a, $b, false, false => $($ret),*);
113106
}
114107
};
115108
(@maybe_opaque $name:ident($op:expr, $a:expr, $b:expr, $c:expr => $($ret:expr),* $(; $($_r3:tt)*)?)) => {
116109
paste::paste! {
117-
matrix_tests!([<$name _opaque>] = |jit| run_test_case(
118-
&TestCase {
119-
bytecode: &bytecode_ternop_opaque($op, $a, $b, $c),
120-
expected_stack: &[$($ret),*],
121-
expected_memory: MEMORY_WHAT_INTERPRETER_SAYS,
122-
expected_gas: GAS_WHAT_INTERPRETER_SAYS,
123-
..Default::default()
124-
},
125-
jit,
126-
));
110+
tests!(@mixed_ternop [<$name _const_const_dyn>] $op, $a, $b, $c, true, true, false => $($ret),*);
111+
tests!(@mixed_ternop [<$name _const_dyn_const>] $op, $a, $b, $c, true, false, true => $($ret),*);
112+
tests!(@mixed_ternop [<$name _const_dyn_dyn>] $op, $a, $b, $c, true, false, false => $($ret),*);
113+
tests!(@mixed_ternop [<$name _dyn_const_const>] $op, $a, $b, $c, false, true, true => $($ret),*);
114+
tests!(@mixed_ternop [<$name _dyn_const_dyn>] $op, $a, $b, $c, false, true, false => $($ret),*);
115+
tests!(@mixed_ternop [<$name _dyn_dyn_const>] $op, $a, $b, $c, false, false, true => $($ret),*);
116+
tests!(@mixed_ternop [<$name _dyn_dyn_dyn>] $op, $a, $b, $c, false, false, false => $($ret),*);
127117
}
128118
};
129119

120+
(@mixed_binop $name:ident $op:expr, $a:expr, $b:expr, $a_const:expr, $b_const:expr => $($ret:expr),*) => {
121+
matrix_tests!($name = |jit| run_test_case(
122+
&TestCase {
123+
bytecode: &bytecode_binop_mixed($op, $a, $b, $a_const, $b_const),
124+
expected_stack: &[$($ret),*],
125+
expected_memory: MEMORY_WHAT_INTERPRETER_SAYS,
126+
expected_gas: GAS_WHAT_INTERPRETER_SAYS,
127+
..Default::default()
128+
},
129+
jit,
130+
));
131+
};
132+
133+
(@mixed_ternop $name:ident $op:expr, $a:expr, $b:expr, $c:expr, $a_const:expr, $b_const:expr, $c_const:expr => $($ret:expr),*) => {
134+
matrix_tests!($name = |jit| run_test_case(
135+
&TestCase {
136+
bytecode: &bytecode_ternop_mixed($op, $a, $b, $c, $a_const, $b_const, $c_const),
137+
expected_stack: &[$($ret),*],
138+
expected_memory: MEMORY_WHAT_INTERPRETER_SAYS,
139+
expected_gas: GAS_WHAT_INTERPRETER_SAYS,
140+
..Default::default()
141+
},
142+
jit,
143+
));
144+
};
145+
130146
(@case @raw { $($fields:tt)* }) => { &TestCase { $($fields)* ..Default::default() } };
131147

132148
(@case $op:expr $(, $args:expr)* $(,)? => $($ret:expr),* $(,)? $(; op_gas($op_gas:expr))?) => {

crates/revmc-codegen/src/tests/mod.rs

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,14 @@ tests! {
694694
exp4(op::EXP, 2_U256, 2_U256 => 4_U256; op_gas(60)),
695695
exp5(op::EXP, 2_U256, 3_U256 => 8_U256; op_gas(60)),
696696
exp6(op::EXP, 2_U256, 4_U256 => 16_U256; op_gas(60)),
697+
exp_zero_base_zero_exp(op::EXP, 0_U256, 0_U256 => 1_U256; op_gas(10)),
698+
exp_zero_base_nonzero_exp(op::EXP, 0_U256, 5_U256 => 0_U256; op_gas(60)),
699+
exp_one_base_large_exp(op::EXP, 1_U256, U256::MAX => 1_U256; op_gas(1610)),
700+
exp_minus_one_base_even_exp(op::EXP, -1_U256, 4_U256 => 1_U256; op_gas(60)),
701+
exp_minus_one_base_odd_exp(op::EXP, -1_U256, 5_U256 => -1_U256; op_gas(60)),
702+
exp_pow4_base_in_range(op::EXP, 4_U256, 63_U256 => 1_U256 << 126; op_gas(60)),
703+
exp_pow4_base_overflow(op::EXP, 4_U256, 128_U256 => 0_U256; op_gas(60)),
704+
exp_non_pow2_base(op::EXP, 3_U256, 5_U256 => 243_U256; op_gas(60)),
697705
exp_overflow(op::EXP, 2_U256, 256_U256 => 0_U256; op_gas(110)),
698706
// Large exponent spanning multiple bytes.
699707
exp_large(op::EXP, 2_U256, 0xFFFF_U256 => 2_U256.pow(0xFFFF_U256); op_gas(110)),
@@ -815,6 +823,12 @@ tests! {
815823
expected_stack: &[KECCAK_EMPTY.into()],
816824
expected_gas: 2 + 3 + gas::KECCAK256,
817825
}),
826+
keccak256_empty_dynamic_offset(@raw {
827+
bytecode: &[op::ADDRESS, op::PUSH0, op::KECCAK256],
828+
expected_return: InstructionResult::InvalidOperandOOG,
829+
expected_stack: STACK_WHAT_INTERPRETER_SAYS,
830+
expected_gas: GAS_WHAT_INTERPRETER_SAYS,
831+
}),
818832
keccak256_1(@raw {
819833
bytecode: &[op::PUSH1, 32, op::PUSH0, op::KECCAK256],
820834
expected_stack: &[keccak256([0; 32]).into()],
@@ -1073,11 +1087,38 @@ tests! {
10731087
expected_stack: &[DEF_ADDR.into_word().into()],
10741088
expected_gas: 5,
10751089
}),
1090+
mload_const_offset_too_large(@raw {
1091+
bytecode: &{
1092+
let mut code = Vec::new();
1093+
code.push(op::PUSH9);
1094+
code.extend_from_slice(&0x1_0000_0000_0000_0000_U256.to_be_bytes::<32>()[23..]);
1095+
code.push(op::MLOAD);
1096+
code
1097+
},
1098+
expected_return: InstructionResult::InvalidOperandOOG,
1099+
expected_stack: &[0x1_0000_0000_0000_0000_U256],
1100+
expected_gas: GAS_WHAT_INTERPRETER_SAYS,
1101+
}),
10761102
mstore1(@raw {
10771103
bytecode: &[op::PUSH0, op::PUSH0, op::MSTORE],
10781104
expected_memory: &[0; 32],
10791105
expected_gas: 2 + 2 + (3 + memory_gas_cost(1)),
10801106
}),
1107+
mstore_const_offset_dyn_value(@raw {
1108+
bytecode: &bytecode_binop_mixed(op::MSTORE, 0_U256, 0x69_U256, true, false),
1109+
expected_memory: MEMORY_WHAT_INTERPRETER_SAYS,
1110+
expected_gas: GAS_WHAT_INTERPRETER_SAYS,
1111+
}),
1112+
mstore_dyn_offset_const_value(@raw {
1113+
bytecode: &bytecode_binop_mixed(op::MSTORE, 0_U256, 0x69_U256, false, true),
1114+
expected_memory: MEMORY_WHAT_INTERPRETER_SAYS,
1115+
expected_gas: GAS_WHAT_INTERPRETER_SAYS,
1116+
}),
1117+
mstore_dyn_offset_dyn_value(@raw {
1118+
bytecode: &bytecode_binop_mixed(op::MSTORE, 0_U256, 0x69_U256, false, false),
1119+
expected_memory: MEMORY_WHAT_INTERPRETER_SAYS,
1120+
expected_gas: GAS_WHAT_INTERPRETER_SAYS,
1121+
}),
10811122
mstore8_1(@raw {
10821123
bytecode: &[op::PUSH0, op::PUSH0, op::MSTORE8],
10831124
expected_memory: &[0; 32],
@@ -1330,6 +1371,12 @@ tests! {
13301371
expected_gas: GAS_WHAT_INTERPRETER_SAYS,
13311372
expected_next_action: ACTION_WHAT_INTERPRETER_SAYS,
13321373
}),
1374+
ret_empty_dynamic_offset(@raw {
1375+
bytecode: &[op::ADDRESS, op::PUSH0, op::RETURN],
1376+
expected_return: InstructionResult::InvalidOperandOOG,
1377+
expected_stack: STACK_WHAT_INTERPRETER_SAYS,
1378+
expected_gas: GAS_WHAT_INTERPRETER_SAYS,
1379+
}),
13331380
ret(@raw {
13341381
bytecode: &[op::PUSH1, 0x69, op::PUSH0, op::MSTORE, op::PUSH1, 32, op::PUSH0, op::RETURN],
13351382
expected_return: InstructionResult::Return,
@@ -2078,48 +2125,55 @@ fn bytecode_unop_opaque(opcode: u8, a: U256) -> Vec<u8> {
20782125
code
20792126
}
20802127

2081-
/// Build bytecode: MSTORE(a, 0), MSTORE(b, 32), MLOAD(32), MLOAD(0), `<op>`
2082-
///
2083-
/// The operands pass through MLOAD (an opaque builtin), preventing the compiler
2084-
/// from constant-folding or exploiting UB at compile time.
2085-
fn bytecode_binop_opaque(opcode: u8, a: U256, b: U256) -> Vec<u8> {
2128+
fn push_const_or_load(code: &mut Vec<u8>, value: U256, is_const: bool, offset: u8) {
2129+
if is_const {
2130+
code.push(op::PUSH32);
2131+
code.extend_from_slice(&value.to_be_bytes::<32>());
2132+
} else {
2133+
code.push(op::PUSH1);
2134+
code.push(offset);
2135+
code.push(op::MLOAD);
2136+
}
2137+
}
2138+
2139+
fn store_dynamic_operands(code: &mut Vec<u8>, operands: &[(U256, bool, u8)]) {
2140+
for &(value, is_const, offset) in operands {
2141+
if !is_const {
2142+
code.push(op::PUSH32);
2143+
code.extend_from_slice(&value.to_be_bytes::<32>());
2144+
code.push(op::PUSH1);
2145+
code.push(offset);
2146+
code.push(op::MSTORE);
2147+
}
2148+
}
2149+
}
2150+
2151+
fn bytecode_binop_mixed(opcode: u8, a: U256, b: U256, a_const: bool, b_const: bool) -> Vec<u8> {
20862152
let mut code = Vec::with_capacity(128);
2087-
code.push(op::PUSH32);
2088-
code.extend_from_slice(&a.to_be_bytes::<32>());
2089-
code.push(op::PUSH1);
2090-
code.push(0x00);
2091-
code.push(op::MSTORE);
2092-
code.push(op::PUSH32);
2093-
code.extend_from_slice(&b.to_be_bytes::<32>());
2094-
code.push(op::PUSH1);
2095-
code.push(0x20);
2096-
code.push(op::MSTORE);
2097-
code.push(op::PUSH1);
2098-
code.push(0x20);
2099-
code.push(op::MLOAD);
2100-
code.push(op::PUSH1);
2101-
code.push(0x00);
2102-
code.push(op::MLOAD);
2153+
store_dynamic_operands(&mut code, &[(a, a_const, 0x00), (b, b_const, 0x20)]);
2154+
push_const_or_load(&mut code, b, b_const, 0x20);
2155+
push_const_or_load(&mut code, a, a_const, 0x00);
21032156
code.push(opcode);
21042157
code
21052158
}
21062159

2107-
/// Build opaque ternop bytecode: MSTORE(a,0), MSTORE(b,32), MSTORE(c,64),
2108-
/// MLOAD(64), MLOAD(32), MLOAD(0), `<op>`.
2109-
fn bytecode_ternop_opaque(opcode: u8, a: U256, b: U256, c: U256) -> Vec<u8> {
2160+
fn bytecode_ternop_mixed(
2161+
opcode: u8,
2162+
a: U256,
2163+
b: U256,
2164+
c: U256,
2165+
a_const: bool,
2166+
b_const: bool,
2167+
c_const: bool,
2168+
) -> Vec<u8> {
21102169
let mut code = Vec::with_capacity(192);
2111-
for (val, offset) in [(a, 0u8), (b, 0x20), (c, 0x40)] {
2112-
code.push(op::PUSH32);
2113-
code.extend_from_slice(&val.to_be_bytes::<32>());
2114-
code.push(op::PUSH1);
2115-
code.push(offset);
2116-
code.push(op::MSTORE);
2117-
}
2118-
for offset in [0x40u8, 0x20, 0x00] {
2119-
code.push(op::PUSH1);
2120-
code.push(offset);
2121-
code.push(op::MLOAD);
2122-
}
2170+
store_dynamic_operands(
2171+
&mut code,
2172+
&[(a, a_const, 0x00), (b, b_const, 0x20), (c, c_const, 0x40)],
2173+
);
2174+
push_const_or_load(&mut code, c, c_const, 0x40);
2175+
push_const_or_load(&mut code, b, b_const, 0x20);
2176+
push_const_or_load(&mut code, a, a_const, 0x00);
21232177
code.push(opcode);
21242178
code
21252179
}

0 commit comments

Comments
 (0)