Skip to content

Commit a2279f2

Browse files
authored
perf(interpreter): rewrite gas accounting for memory expansion (bluealloy#1361)
* feat(interpreter): add helpers for spending all gas * perf(interpreter): rewrite gas accounting for memory expansion * chore: do not inline * chore: restore Cargo.toml
1 parent 3f5bb76 commit a2279f2

File tree

7 files changed

+89
-102
lines changed

7 files changed

+89
-102
lines changed

bins/revm-test/src/bin/snailtracer.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub fn simple_example() {
1919
})
2020
.build();
2121

22-
let _ = evm.transact();
22+
let _ = evm.transact().unwrap();
2323
}
2424

2525
fn main() {

crates/interpreter/src/gas.rs

+8-32
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ pub struct Gas {
1414
limit: u64,
1515
/// The remaining gas.
1616
remaining: u64,
17-
/// The remaining gas, without memory expansion.
18-
remaining_nomem: u64,
19-
/// The **last** memory expansion cost.
20-
memory: u64,
2117
/// Refunded gas. This is used only at the end of execution.
2218
refunded: i64,
2319
}
@@ -29,8 +25,6 @@ impl Gas {
2925
Self {
3026
limit,
3127
remaining: limit,
32-
remaining_nomem: limit,
33-
memory: 0,
3428
refunded: 0,
3529
}
3630
}
@@ -41,8 +35,6 @@ impl Gas {
4135
Self {
4236
limit,
4337
remaining: 0,
44-
remaining_nomem: 0,
45-
memory: 0,
4638
refunded: 0,
4739
}
4840
}
@@ -55,8 +47,11 @@ impl Gas {
5547

5648
/// Returns the **last** memory expansion cost.
5749
#[inline]
50+
#[deprecated = "memory expansion cost is not tracked anymore; \
51+
calculate it using `SharedMemory::current_expansion_cost` instead"]
52+
#[doc(hidden)]
5853
pub const fn memory(&self) -> u64 {
59-
self.memory
54+
0
6055
}
6156

6257
/// Returns the total amount of gas that was refunded.
@@ -87,15 +82,13 @@ impl Gas {
8782
/// Erases a gas cost from the totals.
8883
#[inline]
8984
pub fn erase_cost(&mut self, returned: u64) {
90-
self.remaining_nomem += returned;
9185
self.remaining += returned;
9286
}
9387

9488
/// Spends all remaining gas.
9589
#[inline]
9690
pub fn spend_all(&mut self) {
9791
self.remaining = 0;
98-
self.remaining_nomem = 0;
9992
}
10093

10194
/// Records a refund value.
@@ -128,30 +121,13 @@ impl Gas {
128121
///
129122
/// Returns `false` if the gas limit is exceeded.
130123
#[inline]
124+
#[must_use]
131125
pub fn record_cost(&mut self, cost: u64) -> bool {
132126
let (remaining, overflow) = self.remaining.overflowing_sub(cost);
133-
if overflow {
134-
return false;
135-
}
136-
137-
self.remaining_nomem -= cost;
138-
self.remaining = remaining;
139-
true
140-
}
141-
142-
/// Records memory expansion gas.
143-
///
144-
/// Used in [`resize_memory!`](crate::resize_memory).
145-
#[inline]
146-
pub fn record_memory(&mut self, gas_memory: u64) -> bool {
147-
if gas_memory > self.memory {
148-
let (remaining, overflow) = self.remaining_nomem.overflowing_sub(gas_memory);
149-
if overflow {
150-
return false;
151-
}
152-
self.memory = gas_memory;
127+
let success = !overflow;
128+
if success {
153129
self.remaining = remaining;
154130
}
155-
true
131+
success
156132
}
157133
}

crates/interpreter/src/gas/calc.rs

+10-5
Original file line numberDiff line numberDiff line change
@@ -342,13 +342,18 @@ pub const fn warm_cold_cost(is_cold: bool) -> u64 {
342342
}
343343
}
344344

345-
/// Memory expansion cost calculation.
345+
/// Memory expansion cost calculation for a given memory length.
346346
#[inline]
347-
pub const fn memory_gas(a: usize) -> u64 {
348-
let a = a as u64;
347+
pub const fn memory_gas_for_len(len: usize) -> u64 {
348+
memory_gas(crate::interpreter::num_words(len as u64))
349+
}
350+
351+
/// Memory expansion cost calculation for a given number of words.
352+
#[inline]
353+
pub const fn memory_gas(num_words: u64) -> u64 {
349354
MEMORY
350-
.saturating_mul(a)
351-
.saturating_add(a.saturating_mul(a) / 512)
355+
.saturating_mul(num_words)
356+
.saturating_add(num_words.saturating_mul(num_words) / 512)
352357
}
353358

354359
/// Initial gas that is deducted for transaction to be included.

crates/interpreter/src/instructions/macros.rs

+9-13
Original file line numberDiff line numberDiff line change
@@ -89,27 +89,23 @@ macro_rules! resize_memory {
8989
$crate::resize_memory!($interp, $offset, $len, ())
9090
};
9191
($interp:expr, $offset:expr, $len:expr, $ret:expr) => {
92-
let size = $offset.saturating_add($len);
93-
if size > $interp.shared_memory.len() {
94-
// We are fine with saturating to usize if size is close to MAX value.
95-
let rounded_size = $crate::interpreter::next_multiple_of_32(size);
96-
92+
let new_size = $offset.saturating_add($len);
93+
if new_size > $interp.shared_memory.len() {
9794
#[cfg(feature = "memory_limit")]
98-
if $interp.shared_memory.limit_reached(size) {
95+
if $interp.shared_memory.limit_reached(new_size) {
9996
$interp.instruction_result = $crate::InstructionResult::MemoryLimitOOG;
10097
return $ret;
10198
}
10299

103-
// Gas is calculated in evm words (256 bits).
104-
let words_num = rounded_size / 32;
105-
if !$interp
106-
.gas
107-
.record_memory($crate::gas::memory_gas(words_num))
108-
{
100+
// Note: we can't use `Interpreter` directly here because of potential double-borrows.
101+
if !$crate::interpreter::resize_memory(
102+
&mut $interp.shared_memory,
103+
&mut $interp.gas,
104+
new_size,
105+
) {
109106
$interp.instruction_result = $crate::InstructionResult::MemoryOOG;
110107
return $ret;
111108
}
112-
$interp.shared_memory.resize(rounded_size);
113109
}
114110
};
115111
}

crates/interpreter/src/interpreter.rs

+25-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ mod shared_memory;
66
mod stack;
77

88
pub use contract::Contract;
9-
pub use shared_memory::{next_multiple_of_32, SharedMemory, EMPTY_SHARED_MEMORY};
9+
pub use shared_memory::{num_words, SharedMemory, EMPTY_SHARED_MEMORY};
1010
pub use stack::{Stack, STACK_LIMIT};
1111

1212
use crate::EOFCreateOutcome;
1313
use crate::{
14-
primitives::Bytes, push, push_b256, return_ok, return_revert, CallOutcome, CreateOutcome,
14+
gas, primitives::Bytes, push, push_b256, return_ok, return_revert, CallOutcome, CreateOutcome,
1515
FunctionStack, Gas, Host, InstructionResult, InterpreterAction,
1616
};
1717
use core::cmp::min;
@@ -379,6 +379,13 @@ impl Interpreter {
379379
},
380380
}
381381
}
382+
383+
/// Resize the memory to the new size. Returns whether the gas was enough to resize the memory.
384+
#[inline]
385+
#[must_use]
386+
pub fn resize_memory(&mut self, new_size: usize) -> bool {
387+
resize_memory(&mut self.shared_memory, &mut self.gas, new_size)
388+
}
382389
}
383390

384391
impl InterpreterResult {
@@ -401,6 +408,22 @@ impl InterpreterResult {
401408
}
402409
}
403410

411+
/// Resize the memory to the new size. Returns whether the gas was enough to resize the memory.
412+
#[inline(never)]
413+
#[cold]
414+
#[must_use]
415+
pub fn resize_memory(memory: &mut SharedMemory, gas: &mut Gas, new_size: usize) -> bool {
416+
let new_words = num_words(new_size as u64);
417+
let new_cost = gas::memory_gas(new_words);
418+
let current_cost = memory.current_expansion_cost();
419+
let cost = new_cost - current_cost;
420+
let success = gas.record_cost(cost);
421+
if success {
422+
memory.resize((new_words as usize) * 32);
423+
}
424+
success
425+
}
426+
404427
#[cfg(test)]
405428
mod tests {
406429
use super::*;

crates/interpreter/src/interpreter/shared_memory.rs

+35-48
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1+
use core::{cmp::min, fmt, ops::Range};
12
use revm_primitives::{B256, U256};
2-
3-
use core::{
4-
cmp::min,
5-
fmt,
6-
ops::{BitAnd, Not, Range},
7-
};
83
use std::vec::Vec;
94

105
/// A sequential memory shared between calls, which uses
@@ -128,6 +123,12 @@ impl SharedMemory {
128123
self.len() == 0
129124
}
130125

126+
/// Returns the gas cost for the current memory expansion.
127+
#[inline]
128+
pub fn current_expansion_cost(&self) -> u64 {
129+
crate::gas::memory_gas_for_len(self.len())
130+
}
131+
131132
/// Resizes the memory in-place so that `len` is equal to `new_len`.
132133
#[inline]
133134
pub fn resize(&mut self, new_size: usize) {
@@ -145,21 +146,18 @@ impl SharedMemory {
145146
self.slice_range(offset..offset + size)
146147
}
147148

149+
/// Returns a byte slice of the memory region at the given offset.
150+
///
151+
/// # Panics
152+
///
153+
/// Panics on out of bounds.
148154
#[inline]
149155
#[cfg_attr(debug_assertions, track_caller)]
150-
pub fn slice_range(&self, range: Range<usize>) -> &[u8] {
151-
let last_checkpoint = self.last_checkpoint;
152-
153-
self.buffer
154-
.get(last_checkpoint + range.start..last_checkpoint + range.end)
155-
.unwrap_or_else(|| {
156-
debug_unreachable!(
157-
"slice OOB: {}..{}; len: {}",
158-
range.start,
159-
range.end,
160-
self.len()
161-
)
162-
})
156+
pub fn slice_range(&self, range @ Range { start, end }: Range<usize>) -> &[u8] {
157+
match self.context_memory().get(range) {
158+
Some(slice) => slice,
159+
None => debug_unreachable!("slice OOB: {start}..{end}; len: {}", self.len()),
160+
}
163161
}
164162

165163
/// Returns a byte slice of the memory region at the given offset.
@@ -170,13 +168,11 @@ impl SharedMemory {
170168
#[inline]
171169
#[cfg_attr(debug_assertions, track_caller)]
172170
pub fn slice_mut(&mut self, offset: usize, size: usize) -> &mut [u8] {
173-
let len = self.len();
174171
let end = offset + size;
175-
let last_checkpoint = self.last_checkpoint;
176-
177-
self.buffer
178-
.get_mut(last_checkpoint + offset..last_checkpoint + offset + size)
179-
.unwrap_or_else(|| debug_unreachable!("slice OOB: {offset}..{end}; len: {}", len))
172+
match self.context_memory_mut().get_mut(offset..end) {
173+
Some(slice) => slice,
174+
None => debug_unreachable!("slice OOB: {offset}..{end}"),
175+
}
180176
}
181177

182178
/// Returns the byte at the given offset.
@@ -312,37 +308,28 @@ impl SharedMemory {
312308
}
313309
}
314310

315-
/// Rounds up `x` to the closest multiple of 32. If `x % 32 == 0` then `x` is returned. Note, if `x`
316-
/// is greater than `usize::MAX - 31` this will return `usize::MAX` which isn't a multiple of 32.
311+
/// Returns number of words what would fit to provided number of bytes,
312+
/// i.e. it rounds up the number bytes to number of words.
317313
#[inline]
318-
pub fn next_multiple_of_32(x: usize) -> usize {
319-
let r = x.bitand(31).not().wrapping_add(1).bitand(31);
320-
x.saturating_add(r)
314+
pub const fn num_words(len: u64) -> u64 {
315+
len.saturating_add(31) / 32
321316
}
322317

323318
#[cfg(test)]
324319
mod tests {
325320
use super::*;
326321

327322
#[test]
328-
fn test_next_multiple_of_32() {
329-
// next_multiple_of_32 returns x when it is a multiple of 32
330-
for i in 0..32 {
331-
let x = i * 32;
332-
assert_eq!(x, next_multiple_of_32(x));
333-
}
334-
335-
// next_multiple_of_32 rounds up to the nearest multiple of 32 when `x % 32 != 0`
336-
for x in 0..1024 {
337-
if x % 32 == 0 {
338-
continue;
339-
}
340-
let next_multiple = x + 32 - (x % 32);
341-
assert_eq!(next_multiple, next_multiple_of_32(x));
342-
}
343-
344-
// We expect large values to saturate and not overflow.
345-
assert_eq!(usize::MAX, next_multiple_of_32(usize::MAX));
323+
fn test_num_words() {
324+
assert_eq!(num_words(0), 0);
325+
assert_eq!(num_words(1), 1);
326+
assert_eq!(num_words(31), 1);
327+
assert_eq!(num_words(32), 1);
328+
assert_eq!(num_words(33), 2);
329+
assert_eq!(num_words(63), 2);
330+
assert_eq!(num_words(64), 2);
331+
assert_eq!(num_words(65), 3);
332+
assert_eq!(num_words(u64::MAX), u64::MAX / 32);
346333
}
347334

348335
#[test]

crates/interpreter/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub use gas::Gas;
3434
pub use host::{DummyHost, Host, LoadAccountResult, SStoreResult, SelfDestructResult};
3535
pub use instruction_result::*;
3636
pub use interpreter::{
37-
analysis, next_multiple_of_32, Contract, Interpreter, InterpreterResult, SharedMemory, Stack,
37+
analysis, num_words, Contract, Interpreter, InterpreterResult, SharedMemory, Stack,
3838
EMPTY_SHARED_MEMORY, STACK_LIMIT,
3939
};
4040
pub use interpreter_action::{

0 commit comments

Comments
 (0)