Skip to content

perf(levm): new memory model #3564

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

Merged
merged 46 commits into from
Jul 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
2680471
new memory progress
edg-l Jul 8, 2025
4a01b9e
done
edg-l Jul 8, 2025
e7c80b7
Merge branch 'main' into new_memory
edg-l Jul 8, 2025
4387a0c
fix
edg-l Jul 8, 2025
2fa2694
fix
edg-l Jul 8, 2025
a7c23fe
improve
edg-l Jul 8, 2025
ee33f25
Merge branch 'main' into new_memory
edg-l Jul 9, 2025
62ef32f
better
edg-l Jul 9, 2025
8f5ed9d
fix mcopy
edg-l Jul 9, 2025
b44d5bc
fill instead of truncate
edg-l Jul 9, 2025
f253bab
remove dbg
edg-l Jul 9, 2025
72e08b6
fix
edg-l Jul 9, 2025
74a289a
better handling
edg-l Jul 9, 2025
7a4f252
improve
edg-l Jul 9, 2025
cb33cee
fix and better perf
edg-l Jul 10, 2025
de4520d
Merge remote-tracking branch 'origin/main' into new_memory
edg-l Jul 10, 2025
87a255b
Merge branch 'main' into new_memory
edg-l Jul 14, 2025
37d71b8
fixes
edg-l Jul 14, 2025
f8a0c49
changelog
edg-l Jul 14, 2025
a710c04
remove unused
edg-l Jul 14, 2025
1ba46a3
rename
edg-l Jul 14, 2025
9de7221
docs
edg-l Jul 14, 2025
b11af48
remove unwraps
edg-l Jul 14, 2025
ace118c
better
edg-l Jul 14, 2025
5e7efd3
improve
edg-l Jul 14, 2025
34d4474
force inline to improve perfomance
edg-l Jul 14, 2025
2a97377
unsafe
edg-l Jul 14, 2025
10df4fb
more unsafe
edg-l Jul 14, 2025
47cfc11
unsafe
edg-l Jul 14, 2025
b025e3d
fmt
edg-l Jul 14, 2025
e22d55b
better
edg-l Jul 14, 2025
f022b08
inline
edg-l Jul 14, 2025
5dbfd83
improve
edg-l Jul 15, 2025
beed672
more
edg-l Jul 15, 2025
981da2c
experimental
edg-l Jul 15, 2025
4d67f60
Merge branch 'main' into new_memory
edg-l Jul 15, 2025
d54da5b
Merge remote-tracking branch 'origin/new_memory' into new_memory_unsa…
edg-l Jul 15, 2025
b0aad27
Merge branch 'main' into new_memory
edg-l Jul 15, 2025
4299eb0
Merge branch 'main' into new_memory
edg-l Jul 15, 2025
44a4a3f
Merge branch 'main' into new_memory
edg-l Jul 16, 2025
f332dd1
Merge branch 'main' into new_memory
edg-l Jul 16, 2025
3f5f604
Merge branch 'main' into new_memory
edg-l Jul 16, 2025
4316cf0
Merge branch 'main' into new_memory
edg-l Jul 17, 2025
93373ad
comments
edg-l Jul 17, 2025
f33d78c
format
edg-l Jul 17, 2025
1298156
Merge branch 'main' into new_memory
edg-l Jul 17, 2025
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Perf

### 2025-07-16

- Improve levm memory model [#3564](https://github.com/lambdaclass/ethrex/pull/3564)
-
### 2025-07-15

- Add sstore bench [#3552](https://github.com/lambdaclass/ethrex/pull/3552)
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/levm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ ethereum_foundation_tests = []
debug = []

[lints.rust]
unsafe_code = "forbid"
unsafe_code = "warn"
warnings = "warn"
rust_2018_idioms = "warn"

Expand Down
3 changes: 2 additions & 1 deletion crates/vm/levm/bench/revm_comparison/src/levm_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ pub fn run_with_levm(contract_code: &str, runs: u64, calldata: &str) {
}
let mut vm = init_vm(&mut db, 0, calldata.clone());
let tx_report = black_box(vm.stateless_execute().unwrap());
assert!(tx_report.is_success());

assert!(tx_report.is_success(), "{:?}", tx_report.result);

match tx_report.result {
TxResult::Success => {
Expand Down
65 changes: 47 additions & 18 deletions crates/vm/levm/src/call_frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,34 @@ pub struct Stack {
}

impl Stack {
#[inline]
pub fn pop<const N: usize>(&mut self) -> Result<&[U256; N], ExceptionalHalt> {
// Compile-time check for stack underflow.
if N > STACK_LIMIT {
return Err(ExceptionalHalt::StackUnderflow);
const {
assert!(N <= STACK_LIMIT);
}

// The following operation can never overflow as both `self.offset` and N are within
// STACK_LIMIT (1024).
#[expect(clippy::arithmetic_side_effects)]
let next_offset = self.offset + N;
let next_offset = self.offset.wrapping_add(N);

// The index cannot fail because `self.offset` is known to be valid. The `first_chunk()`
// method will ensure that `next_offset` is within `STACK_LIMIT`, so there's no need to
// check it again.
#[expect(clippy::indexing_slicing)]
let values = self.values[self.offset..]
.first_chunk::<N>()
.ok_or(ExceptionalHalt::StackUnderflow)?;
#[expect(unsafe_code)]
let values = unsafe {
self.values
.get_unchecked(self.offset..)
.first_chunk::<N>()
.ok_or(ExceptionalHalt::StackUnderflow)?
};
// Due to previous error check in first_chunk, next_offset is guaranteed to be < STACK_LIMIT
self.offset = next_offset;

Ok(values)
}

#[inline]
pub fn push<const N: usize>(&mut self, values: &[U256; N]) -> Result<(), ExceptionalHalt> {
// Since the stack grows downwards, when an offset underflow is detected the stack is
// overflowing.
Expand All @@ -61,8 +66,19 @@ impl Stack {

// The following index cannot fail because `next_offset` has already been checked and
// `self.offset` is known to be within `STACK_LIMIT`.
#[expect(clippy::indexing_slicing)]
self.values[next_offset..self.offset].copy_from_slice(values);
#[expect(
unsafe_code,
reason = "self.offset < STACK_LIMIT and next_offset == self.offset - N >= 0"
)]
unsafe {
std::ptr::copy_nonoverlapping(
values.as_ptr(),
self.values
.get_unchecked_mut(next_offset..self.offset)
.as_mut_ptr(),
N,
);
}
self.offset = next_offset;

Ok(())
Expand All @@ -84,12 +100,16 @@ impl Stack {
pub fn get(&self, index: usize) -> Result<&U256, ExceptionalHalt> {
// The following index cannot fail because `self.offset` is known to be within
// `STACK_LIMIT`.
#[expect(clippy::indexing_slicing)]
self.values[self.offset..]
.get(index)
.ok_or(ExceptionalHalt::StackUnderflow)
#[expect(unsafe_code)]
unsafe {
self.values
.get_unchecked(self.offset..)
.get(index)
.ok_or(ExceptionalHalt::StackUnderflow)
}
}

#[inline(always)]
pub fn swap(&mut self, index: usize) -> Result<(), ExceptionalHalt> {
let index = self
.offset
Expand Down Expand Up @@ -134,7 +154,7 @@ impl fmt::Debug for Stack {
}
}

#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default)]
/// A call frame, or execution environment, is the context in which
/// the EVM is currently executing.
/// One context can trigger another with opcodes like CALL or CREATE.
Expand Down Expand Up @@ -176,7 +196,7 @@ pub struct CallFrame {
/// Everytime we want to write an account during execution of a callframe we store the pre-write state so that we can restore if it reverts
pub call_frame_backup: CallFrameBackup,
/// Return data offset
pub ret_offset: U256,
pub ret_offset: usize,
/// Return data size
pub ret_size: usize,
/// If true then transfer value from caller to callee
Expand Down Expand Up @@ -221,6 +241,9 @@ impl CallFrameBackup {

impl CallFrame {
#[allow(clippy::too_many_arguments)]
// Force inline, due to lot of arguments, inlining must be forced, and it is actually beneficial
// because passing so much data is costly. Verified with samply.
#[inline(always)]
pub fn new(
msg_sender: Address,
to: Address,
Expand All @@ -233,12 +256,14 @@ impl CallFrame {
depth: usize,
should_transfer_value: bool,
is_create: bool,
ret_offset: U256,
ret_offset: usize,
ret_size: usize,
stack: Stack,
memory: Memory,
) -> Self {
let invalid_jump_destinations =
get_invalid_jump_destinations(&bytecode).unwrap_or_default();
// Note: Do not use ..Default::default() because it has runtime cost.
Self {
gas_limit,
gas_remaining: gas_limit,
Expand All @@ -256,7 +281,11 @@ impl CallFrame {
ret_offset,
ret_size,
stack,
..Default::default()
memory,
call_frame_backup: CallFrameBackup::default(),
output: Bytes::default(),
pc: 0,
sub_return_data: Bytes::default(),
}
}

Expand Down
64 changes: 35 additions & 29 deletions crates/vm/levm/src/execution_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,22 @@ impl<'a> VM<'a> {
}

pub fn execute_opcode(&mut self, opcode: Opcode) -> Result<OpcodeResult, VMError> {
// The match order matters because it's evaluated top down. So the most frequent opcodes should be near the top.
// A future improvement could be to use a function pointer table.
match opcode {
Opcode::STOP => Ok(OpcodeResult::Halt),
Opcode::PUSH32 => self.op_push::<32>(),
Opcode::MLOAD => self.op_mload(),
Opcode::MSTORE => self.op_mstore(),
Opcode::MSTORE8 => self.op_mstore8(),
Opcode::JUMP => self.op_jump(),
Opcode::SLOAD => self.op_sload(),
Opcode::SSTORE => self.op_sstore(),
Opcode::MSIZE => self.op_msize(),
Opcode::GAS => self.op_gas(),
Opcode::MCOPY => self.op_mcopy(),
Opcode::PUSH0 => self.op_push0(),
Opcode::PUSH1 => self.op_push::<1>(),
Opcode::POP => self.op_pop(),
Opcode::ADD => self.op_add(),
Opcode::MUL => self.op_mul(),
Opcode::SUB => self.op_sub(),
Expand All @@ -52,6 +66,22 @@ impl<'a> VM<'a> {
Opcode::ADDMOD => self.op_addmod(),
Opcode::MULMOD => self.op_mulmod(),
Opcode::EXP => self.op_exp(),
Opcode::CALL => self.op_call(),
Opcode::CALLCODE => self.op_callcode(),
Opcode::RETURN => self.op_return(),
Opcode::DELEGATECALL => self.op_delegatecall(),
Opcode::STATICCALL => self.op_staticcall(),
Opcode::CREATE => self.op_create(),
Opcode::CREATE2 => self.op_create2(),
Opcode::JUMPI => self.op_jumpi(),
Opcode::JUMPDEST => self.op_jumpdest(),
Opcode::ADDRESS => self.op_address(),
Opcode::ORIGIN => self.op_origin(),
Opcode::BALANCE => self.op_balance(),
Opcode::CALLER => self.op_caller(),
Opcode::CALLVALUE => self.op_callvalue(),
Opcode::CODECOPY => self.op_codecopy(),
Opcode::STOP => Ok(OpcodeResult::Halt),
Opcode::SIGNEXTEND => self.op_signextend(),
Opcode::LT => self.op_lt(),
Opcode::GT => self.op_gt(),
Expand All @@ -65,9 +95,6 @@ impl<'a> VM<'a> {
Opcode::CALLDATACOPY => self.op_calldatacopy(),
Opcode::RETURNDATASIZE => self.op_returndatasize(),
Opcode::RETURNDATACOPY => self.op_returndatacopy(),
Opcode::JUMP => self.op_jump(),
Opcode::JUMPI => self.op_jumpi(),
Opcode::JUMPDEST => self.op_jumpdest(),
Opcode::PC => self.op_pc(),
Opcode::BLOCKHASH => self.op_blockhash(),
Opcode::COINBASE => self.op_coinbase(),
Expand All @@ -79,8 +106,6 @@ impl<'a> VM<'a> {
Opcode::BASEFEE => self.op_basefee(),
Opcode::BLOBHASH => self.op_blobhash(),
Opcode::BLOBBASEFEE => self.op_blobbasefee(),
Opcode::PUSH0 => self.op_push0(),
Opcode::PUSH1 => self.op_push::<1>(),
Opcode::PUSH2 => self.op_push::<2>(),
Opcode::PUSH3 => self.op_push::<3>(),
Opcode::PUSH4 => self.op_push::<4>(),
Expand Down Expand Up @@ -111,7 +136,6 @@ impl<'a> VM<'a> {
Opcode::PUSH29 => self.op_push::<29>(),
Opcode::PUSH30 => self.op_push::<30>(),
Opcode::PUSH31 => self.op_push::<31>(),
Opcode::PUSH32 => self.op_push::<32>(),
Opcode::AND => self.op_and(),
Opcode::OR => self.op_or(),
Opcode::XOR => self.op_xor(),
Expand All @@ -132,36 +156,16 @@ impl<'a> VM<'a> {
#[expect(clippy::arithmetic_side_effects, clippy::as_conversions)]
self.op_swap(op as usize - Opcode::SWAP1 as usize + 1)
}
Opcode::POP => self.op_pop(),

Opcode::LOG0 => self.op_log::<0>(),
Opcode::LOG1 => self.op_log::<1>(),
Opcode::LOG2 => self.op_log::<2>(),
Opcode::LOG3 => self.op_log::<3>(),
Opcode::LOG4 => self.op_log::<4>(),
Opcode::MLOAD => self.op_mload(),
Opcode::MSTORE => self.op_mstore(),
Opcode::MSTORE8 => self.op_mstore8(),
Opcode::SLOAD => self.op_sload(),
Opcode::SSTORE => self.op_sstore(),
Opcode::MSIZE => self.op_msize(),
Opcode::GAS => self.op_gas(),
Opcode::MCOPY => self.op_mcopy(),
Opcode::CALL => self.op_call(),
Opcode::CALLCODE => self.op_callcode(),
Opcode::RETURN => self.op_return(),
Opcode::DELEGATECALL => self.op_delegatecall(),
Opcode::STATICCALL => self.op_staticcall(),
Opcode::CREATE => self.op_create(),
Opcode::CREATE2 => self.op_create2(),
Opcode::TLOAD => self.op_tload(),
Opcode::TSTORE => self.op_tstore(),
Opcode::SELFBALANCE => self.op_selfbalance(),
Opcode::ADDRESS => self.op_address(),
Opcode::ORIGIN => self.op_origin(),
Opcode::BALANCE => self.op_balance(),
Opcode::CALLER => self.op_caller(),
Opcode::CALLVALUE => self.op_callvalue(),
Opcode::CODECOPY => self.op_codecopy(),

Opcode::CODESIZE => self.op_codesize(),
Opcode::GASPRICE => self.op_gasprice(),
Opcode::EXTCODESIZE => self.op_extcodesize(),
Expand All @@ -175,6 +179,7 @@ impl<'a> VM<'a> {
}
}

#[cold] // used in the hot path loop, called only really once.
pub fn handle_opcode_result(&mut self) -> Result<ContextResult, VMError> {
// On successful create check output validity
if self.is_create()? {
Expand Down Expand Up @@ -218,6 +223,7 @@ impl<'a> VM<'a> {
})
}

#[cold] // used in the hot path loop, called only really once.
pub fn handle_opcode_error(&mut self, error: VMError) -> Result<ContextResult, VMError> {
if error.should_propagate() {
return Err(error);
Expand Down
Loading
Loading