Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(interpreter): rewrite gas accounting for memory expansion #1361

Merged
merged 5 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[profile.release]
lto = true
codegen-units = 1
debug = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert those?


[profile.ethtests]
inherits = "test"
Expand Down
2 changes: 1 addition & 1 deletion bins/revm-test/src/bin/snailtracer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub fn simple_example() {
})
.build();

let _ = evm.transact();
let _ = evm.transact().unwrap();
}

fn main() {
Expand Down
40 changes: 8 additions & 32 deletions crates/interpreter/src/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ pub struct Gas {
limit: u64,
/// The remaining gas.
remaining: u64,
/// The remaining gas, without memory expansion.
remaining_nomem: u64,
/// The **last** memory expansion cost.
memory: u64,
/// Refunded gas. This is used only at the end of execution.
refunded: i64,
}
Expand All @@ -29,8 +25,6 @@ impl Gas {
Self {
limit,
remaining: limit,
remaining_nomem: limit,
memory: 0,
refunded: 0,
}
}
Expand All @@ -41,8 +35,6 @@ impl Gas {
Self {
limit,
remaining: 0,
remaining_nomem: 0,
memory: 0,
refunded: 0,
}
}
Expand All @@ -55,8 +47,11 @@ impl Gas {

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

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

/// Spends all remaining gas.
#[inline]
pub fn spend_all(&mut self) {
self.remaining = 0;
self.remaining_nomem = 0;
}

/// Records a refund value.
Expand Down Expand Up @@ -128,30 +121,13 @@ impl Gas {
///
/// Returns `false` if the gas limit is exceeded.
#[inline]
#[must_use]
pub fn record_cost(&mut self, cost: u64) -> bool {
let (remaining, overflow) = self.remaining.overflowing_sub(cost);
if overflow {
return false;
}

self.remaining_nomem -= cost;
self.remaining = remaining;
true
}

/// Records memory expansion gas.
///
/// Used in [`resize_memory!`](crate::resize_memory).
#[inline]
pub fn record_memory(&mut self, gas_memory: u64) -> bool {
if gas_memory > self.memory {
let (remaining, overflow) = self.remaining_nomem.overflowing_sub(gas_memory);
if overflow {
return false;
}
self.memory = gas_memory;
let success = !overflow;
if success {
self.remaining = remaining;
}
true
success
}
}
15 changes: 10 additions & 5 deletions crates/interpreter/src/gas/calc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,13 +342,18 @@ pub const fn warm_cold_cost(is_cold: bool) -> u64 {
}
}

/// Memory expansion cost calculation.
/// Memory expansion cost calculation for a given memory length.
#[inline]
pub const fn memory_gas(a: usize) -> u64 {
let a = a as u64;
pub const fn memory_gas_for_len(len: usize) -> u64 {
memory_gas(crate::interpreter::num_words(len as u64))
}

/// Memory expansion cost calculation for a given number of words.
#[inline]
pub const fn memory_gas(num_words: u64) -> u64 {
MEMORY
.saturating_mul(a)
.saturating_add(a.saturating_mul(a) / 512)
.saturating_mul(num_words)
.saturating_add(num_words.saturating_mul(num_words) / 512)
}

/// Initial gas that is deducted for transaction to be included.
Expand Down
22 changes: 9 additions & 13 deletions crates/interpreter/src/instructions/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,27 +89,23 @@ macro_rules! resize_memory {
$crate::resize_memory!($interp, $offset, $len, ())
};
($interp:expr, $offset:expr, $len:expr, $ret:expr) => {
let size = $offset.saturating_add($len);
if size > $interp.shared_memory.len() {
// We are fine with saturating to usize if size is close to MAX value.
let rounded_size = $crate::interpreter::next_multiple_of_32(size);

let new_size = $offset.saturating_add($len);
if new_size > $interp.shared_memory.len() {
#[cfg(feature = "memory_limit")]
if $interp.shared_memory.limit_reached(size) {
if $interp.shared_memory.limit_reached(new_size) {
$interp.instruction_result = $crate::InstructionResult::MemoryLimitOOG;
return $ret;
}

// Gas is calculated in evm words (256 bits).
let words_num = rounded_size / 32;
if !$interp
.gas
.record_memory($crate::gas::memory_gas(words_num))
{
// Note: we can't use `Interpreter` directly here because of potential double-borrows.
if !$crate::interpreter::resize_memory(
&mut $interp.shared_memory,
&mut $interp.gas,
new_size,
) {
$interp.instruction_result = $crate::InstructionResult::MemoryOOG;
return $ret;
}
$interp.shared_memory.resize(rounded_size);
}
};
}
Expand Down
27 changes: 25 additions & 2 deletions crates/interpreter/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ mod shared_memory;
mod stack;

pub use contract::Contract;
pub use shared_memory::{next_multiple_of_32, SharedMemory, EMPTY_SHARED_MEMORY};
pub use shared_memory::{num_words, SharedMemory, EMPTY_SHARED_MEMORY};
pub use stack::{Stack, STACK_LIMIT};

use crate::EOFCreateOutcome;
use crate::{
primitives::Bytes, push, push_b256, return_ok, return_revert, CallOutcome, CreateOutcome,
gas, primitives::Bytes, push, push_b256, return_ok, return_revert, CallOutcome, CreateOutcome,
FunctionStack, Gas, Host, InstructionResult, InterpreterAction,
};
use core::cmp::min;
Expand Down Expand Up @@ -379,6 +379,13 @@ impl Interpreter {
},
}
}

/// Resize the memory to the new size. Returns whether the gas was enough to resize the memory.
#[inline]
#[must_use]
pub fn resize_memory(&mut self, new_size: usize) -> bool {
resize_memory(&mut self.shared_memory, &mut self.gas, new_size)
}
}

impl InterpreterResult {
Expand All @@ -401,6 +408,22 @@ impl InterpreterResult {
}
}

/// Resize the memory to the new size. Returns whether the gas was enough to resize the memory.
#[inline(never)]
#[cold]
#[must_use]
pub fn resize_memory(memory: &mut SharedMemory, gas: &mut Gas, new_size: usize) -> bool {
let new_words = num_words(new_size as u64);
let new_cost = gas::memory_gas(new_words);
let current_cost = memory.current_expansion_cost();
let cost = new_cost - current_cost;
let success = gas.record_cost(cost);
if success {
memory.resize((new_words as usize) * 32);
}
success
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
83 changes: 35 additions & 48 deletions crates/interpreter/src/interpreter/shared_memory.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
use core::{cmp::min, fmt, ops::Range};
use revm_primitives::{B256, U256};

use core::{
cmp::min,
fmt,
ops::{BitAnd, Not, Range},
};
use std::vec::Vec;

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

/// Returns the gas cost for the current memory expansion.
#[inline]
pub fn current_expansion_cost(&self) -> u64 {
crate::gas::memory_gas_for_len(self.len())
}

/// Resizes the memory in-place so that `len` is equal to `new_len`.
#[inline]
pub fn resize(&mut self, new_size: usize) {
Expand All @@ -145,21 +146,18 @@ impl SharedMemory {
self.slice_range(offset..offset + size)
}

/// Returns a byte slice of the memory region at the given offset.
///
/// # Panics
///
/// Panics on out of bounds.
#[inline]
#[cfg_attr(debug_assertions, track_caller)]
pub fn slice_range(&self, range: Range<usize>) -> &[u8] {
let last_checkpoint = self.last_checkpoint;

self.buffer
.get(last_checkpoint + range.start..last_checkpoint + range.end)
.unwrap_or_else(|| {
debug_unreachable!(
"slice OOB: {}..{}; len: {}",
range.start,
range.end,
self.len()
)
})
pub fn slice_range(&self, range @ Range { start, end }: Range<usize>) -> &[u8] {
match self.context_memory().get(range) {
Some(slice) => slice,
None => debug_unreachable!("slice OOB: {start}..{end}; len: {}", self.len()),
}
}

/// Returns a byte slice of the memory region at the given offset.
Expand All @@ -170,13 +168,11 @@ impl SharedMemory {
#[inline]
#[cfg_attr(debug_assertions, track_caller)]
pub fn slice_mut(&mut self, offset: usize, size: usize) -> &mut [u8] {
let len = self.len();
let end = offset + size;
let last_checkpoint = self.last_checkpoint;

self.buffer
.get_mut(last_checkpoint + offset..last_checkpoint + offset + size)
.unwrap_or_else(|| debug_unreachable!("slice OOB: {offset}..{end}; len: {}", len))
match self.context_memory_mut().get_mut(offset..end) {
Some(slice) => slice,
None => debug_unreachable!("slice OOB: {offset}..{end}"),
}
}

/// Returns the byte at the given offset.
Expand Down Expand Up @@ -312,37 +308,28 @@ impl SharedMemory {
}
}

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

#[cfg(test)]
mod tests {
use super::*;

#[test]
DaniPopes marked this conversation as resolved.
Show resolved Hide resolved
fn test_next_multiple_of_32() {
// next_multiple_of_32 returns x when it is a multiple of 32
for i in 0..32 {
let x = i * 32;
assert_eq!(x, next_multiple_of_32(x));
}

// next_multiple_of_32 rounds up to the nearest multiple of 32 when `x % 32 != 0`
for x in 0..1024 {
if x % 32 == 0 {
continue;
}
let next_multiple = x + 32 - (x % 32);
assert_eq!(next_multiple, next_multiple_of_32(x));
}

// We expect large values to saturate and not overflow.
assert_eq!(usize::MAX, next_multiple_of_32(usize::MAX));
fn test_num_words() {
assert_eq!(num_words(0), 0);
assert_eq!(num_words(1), 1);
assert_eq!(num_words(31), 1);
assert_eq!(num_words(32), 1);
assert_eq!(num_words(33), 2);
assert_eq!(num_words(63), 2);
assert_eq!(num_words(64), 2);
assert_eq!(num_words(65), 3);
assert_eq!(num_words(u64::MAX), u64::MAX / 32);
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/interpreter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub use gas::Gas;
pub use host::{DummyHost, Host, LoadAccountResult, SStoreResult, SelfDestructResult};
pub use instruction_result::*;
pub use interpreter::{
analysis, next_multiple_of_32, Contract, Interpreter, InterpreterResult, SharedMemory, Stack,
analysis, num_words, Contract, Interpreter, InterpreterResult, SharedMemory, Stack,
EMPTY_SHARED_MEMORY, STACK_LIMIT,
};
pub use interpreter_action::{
Expand Down
Loading