perf: dense multi-version slots for lazy addresses, lockless exec results - WIP#504
perf: dense multi-version slots for lazy addresses, lockless exec results - WIP#504datnguyencse wants to merge 2 commits into
Conversation
|
Gigagas Benchmark
|
There was a problem hiding this comment.
Code Review
This pull request introduces a significant performance optimization for 'lazy addresses' (hot accounts like fee recipients) by implementing a dual-layout multi-version memory structure. It adds a 'Dense' layout using boxed slices and UnsafeCell for O(1) writes and cache-friendly iteration, complementing the existing BTreeMap-based 'Sparse' layout. Additionally, the PR replaces Mutexes with lockless UnsafeCell-based slots for recording execution results and parallelizes the final state application for lazy addresses using Rayon. Feedback was provided to improve error handling consistency when loading account data from storage during the post-processing phase.
| let (mut balance, mut nonce) = storage | ||
| .basic(&address) | ||
| .ok() | ||
| .flatten() | ||
| .map(|acc| (acc.balance, acc.nonce)) | ||
| .unwrap_or_default(); |
There was a problem hiding this comment.
The current implementation uses ok().flatten().unwrap_or_default(), which silently ignores potential storage errors. For consistency with how code_hash is handled just above (lines 287-290), it is better to propagate storage errors explicitly.
let (mut balance, mut nonce) = match storage.basic(&address) {
Ok(Some(acc)) => (acc.balance, acc.nonce),
Ok(None) => (U256::ZERO, 0),
Err(err) => return Err(PevmError::StorageError(err.to_string())),
};
Summary
Two perf changes in the parallel execution path, motivated by profiling raw-transfer blocks where post-processing of lazy addresses (beneficiary updates) dominated wall time:
1. Unified Sparse/Dense multi-version storage (
mv_memory.rs)MvMemory.datawas aDashMap<_, BTreeMap<TxIdx, MemoryEntry>>for everything, plus a separatelazy_recipient_slotsmap for the beneficiary fast path. Replaced with a singleDashMap<_, MvEntries>where:MvEntries::Sparse(BTreeMap)— used for storage slots, code hashes, etc. (sparse writes; needs O(log n) range queries for validation).MvEntries::Dense(Box<[UnsafeCell<Option<MemoryEntry>>]>)— used for lazy addresses, indexed directly bytx_idx. O(1) writes viaUnsafeCell(no DashMap write lock) and cache-friendly sequential iteration for post-processing.add_lazy_addressesatomically upgrades a location from Sparse → Dense under the DashMap shard write lock, migrating existing entries. Thelazy_addressesmutex guarantees the slot is visible by the time any concurrentrecord()reaches the Dense fast path.2. Lazy post-processing in one pass, parallelized state writes (
pevm.rs)Collapsed the dual fast/slow path in
execute_revm_parallel's lazy post-processing into a single Dense path:(balance, nonce)after each tx — required becauseLazySendercarries the nonce-check and balance-underflow checks revm skipped.(balance, nonce)to eachtx_result.state— independent writes acrosstx_idxso no synchronization needed.A
Sparsefallback is kept for correctness but never executes in practice (all lazy addresses are upgraded to Dense before any read).Which reduce the post-execution cost from 10ms to 2ms3. Lockless execution result slots (
pevm.rs)execution_results: Vec<Mutex<Option<PevmTxExecutionResult>>>→Vec<ExecutionResultSlot>whereExecutionResultSlotwrapsUnsafeCell<Option<_>>. The Block-STM scheduler already guarantees at most one writer pertx_idx, so the Mutex was pure overhead (one atomic CAS per tx on the parallel write path, one per tx on the take loop).4. Minor
as_sparse()guard invm.rson the threedata.get().range(..).next_back()call sites; Dense entries are never reached from VM reads becauseis_lazy=trueshort-circuits with a mock account.bench_raw_transferswas usingTxEnv::default()(nonce 0) againstmock_account(nonce 1), causing every tx to fail nonce check.