Compare commits

...

4 Commits

Author SHA1 Message Date
Ubuntu
aabb9c858b fix: resolve CI failures for no_std + rayon and docs
- Collect HashMap entries into Vec before passing to from_bundle_state
  to avoid IntoParallelIterator bound on &HashMap (not satisfied when
  alloy-primitives uses hashbrown without its rayon feature)
- Make ADAPTIVE_THRESHOLD pub so docs can link to it

Amp-Thread-ID: https://ampcode.com/threads/T-019c5361-0f73-7484-874d-cae96032da03
2026-02-12 19:56:48 +00:00
YK
d123ba89ef Merge branch 'main' into adaptive-hashed-post-state 2026-02-12 14:43:48 -05:00
Georgios Konstantopoulos
0cb4df9b9e fix: add non-rayon fallback for from_bundle_state_adaptive
- Add #[cfg(not(feature = "rayon"))] variant that delegates to sequential
  from_bundle_state, fixing compile error when rayon feature is disabled
- Gate ADAPTIVE_THRESHOLD constant behind #[cfg(feature = "rayon")] to
  suppress dead_code warning in non-rayon builds
- Remove unnecessary double HashMap conversion in benchmark
- Trim overly specific timing claims from doc comments

Amp-Thread-ID: https://ampcode.com/threads/T-019c5272-9434-729f-8576-fb9b4372903d
Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019c5272-9434-729f-8576-fb9b4372903d
Co-authored-by: Amp <amp@ampcode.com>
2026-02-12 15:38:36 +00:00
Ubuntu
c405e72f6c perf: adaptive hashing threshold for HashedPostState::from_bundle_state
Add `from_bundle_state_adaptive` that dispatches to sequential iteration
for ≤32 accounts, avoiding rayon's fixed scheduling overhead (~10-15µs).

Microbenchmarks show sequential hashing is ~28% faster for 20 accounts
(23.6µs vs 32.6µs). The crossover point where rayon begins to pay off
is between 20-50 accounts.

Also extract shared hashing logic into `hash_bundle_account` helper to
deduplicate code between the parallel, sequential, and adaptive paths.

Amp-Thread-ID: https://ampcode.com/threads/T-019c5241-0c33-76bc-8cdd-5470f7de5ff4
2026-02-12 15:02:43 +00:00
10 changed files with 66 additions and 38 deletions

View File

@@ -208,9 +208,9 @@ impl<T> ExecutionOutcome<T> {
}
/// Returns [`HashedPostState`] for this execution outcome.
/// See [`HashedPostState::from_bundle_state`] for more info.
/// See [`HashedPostState::from_bundle_state_adaptive`] for more info.
pub fn hash_state_slow<KH: KeyHasher>(&self) -> HashedPostState {
HashedPostState::from_bundle_state::<KH>(&self.bundle.state)
HashedPostState::from_bundle_state_adaptive::<KH>(&self.bundle.state)
}
/// Transform block number to the index of block.

View File

@@ -148,7 +148,7 @@ impl StateProofProvider for StateProviderTest {
impl HashedPostStateProvider for StateProviderTest {
fn hashed_post_state(&self, bundle_state: &revm::database::BundleState) -> HashedPostState {
HashedPostState::from_bundle_state::<KeccakKeyHasher>(bundle_state.state())
HashedPostState::from_bundle_state_adaptive::<KeccakKeyHasher>(bundle_state.state())
}
}

View File

@@ -359,7 +359,7 @@ async fn run_pipeline_forward_and_unwind(
// Convert bundle state to hashed post state and compute state root
let hashed_state =
HashedPostState::from_bundle_state::<KeccakKeyHasher>(output.state.state());
HashedPostState::from_bundle_state_adaptive::<KeccakKeyHasher>(output.state.state());
let (state_root, _trie_updates) = StateRoot::overlay_root_with_updates(
provider.tx_ref(),
&hashed_state.clone().into_sorted(),

View File

@@ -621,7 +621,7 @@ impl<N: ProviderNodeTypes> StateProviderFactory for BlockchainProvider<N> {
impl<N: NodeTypesWithDB> HashedPostStateProvider for BlockchainProvider<N> {
fn hashed_post_state(&self, bundle_state: &BundleState) -> HashedPostState {
HashedPostState::from_bundle_state::<KeccakKeyHasher>(bundle_state.state())
HashedPostState::from_bundle_state_adaptive::<KeccakKeyHasher>(bundle_state.state())
}
}

View File

@@ -719,7 +719,7 @@ impl<N: ProviderNodeTypes> PruneCheckpointReader for ProviderFactory<N> {
impl<N: ProviderNodeTypes> HashedPostStateProvider for ProviderFactory<N> {
fn hashed_post_state(&self, bundle_state: &BundleState) -> HashedPostState {
HashedPostState::from_bundle_state::<KeccakKeyHasher>(bundle_state.state())
HashedPostState::from_bundle_state_adaptive::<KeccakKeyHasher>(bundle_state.state())
}
}

View File

@@ -491,7 +491,7 @@ impl<
impl<Provider> HashedPostStateProvider for HistoricalStateProviderRef<'_, Provider> {
fn hashed_post_state(&self, bundle_state: &revm_database::BundleState) -> HashedPostState {
HashedPostState::from_bundle_state::<KeccakKeyHasher>(bundle_state.state())
HashedPostState::from_bundle_state_adaptive::<KeccakKeyHasher>(bundle_state.state())
}
}

View File

@@ -167,7 +167,7 @@ impl<Provider: DBProvider> StateProofProvider for LatestStateProviderRef<'_, Pro
impl<Provider: DBProvider> HashedPostStateProvider for LatestStateProviderRef<'_, Provider> {
fn hashed_post_state(&self, bundle_state: &revm_database::BundleState) -> HashedPostState {
HashedPostState::from_bundle_state::<KeccakKeyHasher>(bundle_state.state())
HashedPostState::from_bundle_state_adaptive::<KeccakKeyHasher>(bundle_state.state())
}
}

View File

@@ -52,20 +52,7 @@ impl HashedPostState {
) -> Self {
state
.into_par_iter()
.map(|(address, account)| {
let hashed_address = KH::hash_key(address);
let hashed_account = account.info.as_ref().map(Into::into);
let hashed_storage = HashedStorage::from_plain_storage(
account.status,
account.storage.iter().map(|(slot, value)| (slot, &value.present_value)),
);
(
hashed_address,
hashed_account,
(!hashed_storage.is_empty()).then_some(hashed_storage),
)
})
.map(|(address, account)| Self::hash_bundle_account::<KH>(address, account))
.collect()
}
@@ -78,23 +65,57 @@ impl HashedPostState {
) -> Self {
state
.into_iter()
.map(|(address, account)| {
let hashed_address = KH::hash_key(address);
let hashed_account = account.info.as_ref().map(Into::into);
let hashed_storage = HashedStorage::from_plain_storage(
account.status,
account.storage.iter().map(|(slot, value)| (slot, &value.present_value)),
);
(
hashed_address,
hashed_account,
(!hashed_storage.is_empty()).then_some(hashed_storage),
)
})
.map(|(address, account)| Self::hash_bundle_account::<KH>(address, account))
.collect()
}
/// Initialize [`HashedPostState`] from bundle state, choosing between sequential and parallel
/// hashing based on the number of accounts.
///
/// For small account counts (≤ [`Self::ADAPTIVE_THRESHOLD`]), sequential iteration avoids
/// rayon's fixed scheduling overhead, which can exceed the hashing work itself.
#[cfg(feature = "rayon")]
pub fn from_bundle_state_adaptive<KH: KeyHasher>(
state: &HashMap<Address, BundleAccount>,
) -> Self {
if state.len() <= Self::ADAPTIVE_THRESHOLD {
state
.iter()
.map(|(address, account)| Self::hash_bundle_account::<KH>(address, account))
.collect()
} else {
let entries: Vec<_> = state.iter().collect();
Self::from_bundle_state::<KH>(entries)
}
}
/// Initialize [`HashedPostState`] from bundle state sequentially (non-rayon fallback).
#[cfg(not(feature = "rayon"))]
pub fn from_bundle_state_adaptive<KH: KeyHasher>(
state: &HashMap<Address, BundleAccount>,
) -> Self {
Self::from_bundle_state::<KH>(state)
}
/// Account count at or below which sequential hashing outperforms rayon parallel iteration.
#[cfg(feature = "rayon")]
pub const ADAPTIVE_THRESHOLD: usize = 32;
/// Hashes a single bundle account entry into the tuple expected by [`FromIterator`].
fn hash_bundle_account<KH: KeyHasher>(
address: &Address,
account: &BundleAccount,
) -> (B256, Option<Account>, Option<HashedStorage>) {
let hashed_address = KH::hash_key(address);
let hashed_account = account.info.as_ref().map(Into::into);
let hashed_storage = HashedStorage::from_plain_storage(
account.status,
account.storage.iter().map(|(slot, value)| (slot, &value.present_value)),
);
(hashed_address, hashed_account, (!hashed_storage.is_empty()).then_some(hashed_storage))
}
/// Construct [`HashedPostState`] from a single [`HashedStorage`].
pub fn from_hashed_storage(hashed_address: B256, storage: HashedStorage) -> Self {
Self {

View File

@@ -9,7 +9,7 @@ pub fn hash_post_state(c: &mut Criterion) {
let mut group = c.benchmark_group("Hash Post State");
group.sample_size(20);
for size in [100, 1_000, 3_000, 5_000, 10_000] {
for size in [10, 20, 32, 50, 100, 1_000, 3_000, 5_000, 10_000] {
// Too slow.
#[expect(unexpected_cfgs)]
if cfg!(codspeed) && size > 1_000 {
@@ -27,6 +27,13 @@ pub fn hash_post_state(c: &mut Criterion) {
group.bench_function(BenchmarkId::new("parallel hashing", size), |b| {
b.iter(|| HashedPostState::from_bundle_state::<KeccakKeyHasher>(&state))
});
// adaptive
let state_map: alloy_primitives::map::HashMap<Address, BundleAccount> =
state.iter().map(|(k, v)| (*k, v.clone())).collect();
group.bench_function(BenchmarkId::new("adaptive hashing", size), |b| {
b.iter(|| HashedPostState::from_bundle_state_adaptive::<KeccakKeyHasher>(&state_map))
});
}
}

View File

@@ -257,7 +257,7 @@ fn run_case(case: &BlockchainTest) -> Result<(), Error> {
// Compute and check the post state root
let hashed_state =
HashedPostState::from_bundle_state::<KeccakKeyHasher>(output.state.state());
HashedPostState::from_bundle_state_adaptive::<KeccakKeyHasher>(output.state.state());
let (computed_state_root, _) = StateRoot::overlay_root_with_updates(
provider.tx_ref(),
&hashed_state.clone_into_sorted(),