Skip to content

Commit a9bd38a

Browse files
mattsseampagentDaniPopes
authored
perf(trie): parallelize merge_ancestors_into_overlay (#21473)
Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
1 parent a544d24 commit a9bd38a

3 files changed

Lines changed: 88 additions & 26 deletions

File tree

crates/chain-state/src/deferred_trie.rs

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,54 @@ impl DeferredTrieData {
274274
/// In normal operation, the parent always has a cached overlay and this
275275
/// function is never called.
276276
///
277-
/// Iterates ancestors oldest -> newest, then extends with current block's data,
278-
/// so later state takes precedence.
277+
/// When the `rayon` feature is enabled:
278+
/// 1. Collects ancestor data (states and updates)
279+
/// 2. Merges states and trie updates in parallel using k-way merge
280+
#[cfg(feature = "rayon")]
281+
fn merge_ancestors_into_overlay(
282+
ancestors: &[Self],
283+
sorted_hashed_state: &HashedPostStateSorted,
284+
sorted_trie_updates: &TrieUpdatesSorted,
285+
) -> TrieInputSorted {
286+
// Early exit: no ancestors means just wrap current block's data
287+
if ancestors.is_empty() {
288+
return TrieInputSorted::new(
289+
Arc::new(sorted_trie_updates.clone()),
290+
Arc::new(sorted_hashed_state.clone()),
291+
Default::default(),
292+
);
293+
}
294+
295+
// Collect ancestor data in reverse (newest to oldest) for merge_slice
296+
let (states, updates): (Vec<_>, Vec<_>) = ancestors
297+
.iter()
298+
.rev()
299+
.map(|a| {
300+
// Note: we can assume that this trie data has already been computed
301+
let data = a.wait_cloned();
302+
(data.hashed_state, data.trie_updates)
303+
})
304+
.unzip();
305+
306+
// Merge state and nodes in parallel using k-way merge
307+
let (state, nodes) = rayon::join(
308+
|| {
309+
let mut merged = HashedPostStateSorted::merge_slice(&states);
310+
merged.extend_ref_and_sort(sorted_hashed_state);
311+
merged
312+
},
313+
|| {
314+
let mut merged = TrieUpdatesSorted::merge_slice(&updates);
315+
merged.extend_ref_and_sort(sorted_trie_updates);
316+
merged
317+
},
318+
);
319+
320+
TrieInputSorted::new(Arc::new(nodes), Arc::new(state), Default::default())
321+
}
322+
323+
/// Sequential fallback when rayon is not available.
324+
#[cfg(not(feature = "rayon"))]
279325
fn merge_ancestors_into_overlay(
280326
ancestors: &[Self],
281327
sorted_hashed_state: &HashedPostStateSorted,
@@ -293,18 +339,8 @@ impl DeferredTrieData {
293339
nodes_mut.extend_ref_and_sort(ancestor_data.trie_updates.as_ref());
294340
}
295341

296-
// Extend with current block's sorted data last (takes precedence)
297-
#[cfg(feature = "rayon")]
298-
rayon::join(
299-
|| state_mut.extend_ref_and_sort(sorted_hashed_state),
300-
|| nodes_mut.extend_ref_and_sort(sorted_trie_updates),
301-
);
302-
303-
#[cfg(not(feature = "rayon"))]
304-
{
305-
state_mut.extend_ref_and_sort(sorted_hashed_state);
306-
nodes_mut.extend_ref_and_sort(sorted_trie_updates);
307-
}
342+
state_mut.extend_ref_and_sort(sorted_hashed_state);
343+
nodes_mut.extend_ref_and_sort(sorted_trie_updates);
308344

309345
overlay
310346
}

crates/trie/common/src/hashed_state.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -614,16 +614,29 @@ impl HashedPostStateSorted {
614614
/// For small batches, uses `extend_ref_and_sort` loop.
615615
/// For large batches, uses k-way merge for O(n log k) complexity.
616616
pub fn merge_batch<T: AsRef<Self> + From<Self>>(iter: impl IntoIterator<Item = T>) -> T {
617+
let items: alloc::vec::Vec<_> = iter.into_iter().collect();
618+
match items.len() {
619+
0 => Self::default().into(),
620+
1 => items.into_iter().next().expect("len == 1"),
621+
_ => Self::merge_slice(&items).into(),
622+
}
623+
}
624+
625+
/// Batch-merge sorted hashed post states from a slice. Slice is **newest to oldest**.
626+
///
627+
/// This variant takes a slice reference directly, avoiding iterator collection overhead.
628+
/// For small batches, uses `extend_ref_and_sort` loop.
629+
/// For large batches, uses k-way merge for O(n log k) complexity.
630+
pub fn merge_slice<T: AsRef<Self>>(items: &[T]) -> Self {
617631
const THRESHOLD: usize = 30;
618632

619-
let items: alloc::vec::Vec<_> = iter.into_iter().collect();
620633
let k = items.len();
621634

622635
if k == 0 {
623-
return Self::default().into();
636+
return Self::default();
624637
}
625638
if k == 1 {
626-
return items.into_iter().next().expect("k == 1");
639+
return items[0].as_ref().clone();
627640
}
628641

629642
if k < THRESHOLD {
@@ -633,7 +646,7 @@ impl HashedPostStateSorted {
633646
for next in iter {
634647
acc.extend_ref_and_sort(next.as_ref());
635648
}
636-
return acc.into();
649+
return acc;
637650
}
638651

639652
// Large k: k-way merge.
@@ -647,7 +660,7 @@ impl HashedPostStateSorted {
647660

648661
let mut acc: B256Map<StorageAcc<'_>> = B256Map::default();
649662

650-
for item in &items {
663+
for item in items {
651664
for (addr, storage) in &item.as_ref().storages {
652665
let entry = acc.entry(*addr).or_insert_with(|| StorageAcc {
653666
wiped: false,
@@ -675,7 +688,7 @@ impl HashedPostStateSorted {
675688
})
676689
.collect();
677690

678-
Self { accounts, storages }.into()
691+
Self { accounts, storages }
679692
}
680693

681694
/// Clears all accounts and storage data.

crates/trie/common/src/updates.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -632,16 +632,29 @@ impl TrieUpdatesSorted {
632632
/// For small batches, uses `extend_ref_and_sort` loop.
633633
/// For large batches, uses k-way merge for O(n log k) complexity.
634634
pub fn merge_batch<T: AsRef<Self> + From<Self>>(iter: impl IntoIterator<Item = T>) -> T {
635+
let items: alloc::vec::Vec<_> = iter.into_iter().collect();
636+
match items.len() {
637+
0 => Self::default().into(),
638+
1 => items.into_iter().next().expect("len == 1"),
639+
_ => Self::merge_slice(&items).into(),
640+
}
641+
}
642+
643+
/// Batch-merge sorted trie updates from a slice. Slice is **newest to oldest**.
644+
///
645+
/// This variant takes a slice reference directly, avoiding iterator collection overhead.
646+
/// For small batches, uses `extend_ref_and_sort` loop.
647+
/// For large batches, uses k-way merge for O(n log k) complexity.
648+
pub fn merge_slice<T: AsRef<Self>>(items: &[T]) -> Self {
635649
const THRESHOLD: usize = 30;
636650

637-
let items: alloc::vec::Vec<_> = iter.into_iter().collect();
638651
let k = items.len();
639652

640653
if k == 0 {
641-
return Self::default().into();
654+
return Self::default();
642655
}
643656
if k == 1 {
644-
return items.into_iter().next().expect("k == 1");
657+
return items[0].as_ref().clone();
645658
}
646659

647660
if k < THRESHOLD {
@@ -651,7 +664,7 @@ impl TrieUpdatesSorted {
651664
for next in iter {
652665
acc.extend_ref_and_sort(next.as_ref());
653666
}
654-
return acc.into();
667+
return acc;
655668
}
656669

657670
// Large k: k-way merge.
@@ -666,7 +679,7 @@ impl TrieUpdatesSorted {
666679

667680
let mut acc: B256Map<StorageAcc<'_>> = B256Map::default();
668681

669-
for item in &items {
682+
for item in items {
670683
for (addr, storage) in &item.as_ref().storage_tries {
671684
let entry = acc.entry(*addr).or_insert_with(|| StorageAcc {
672685
is_deleted: false,
@@ -695,7 +708,7 @@ impl TrieUpdatesSorted {
695708
})
696709
.collect();
697710

698-
Self { account_nodes, storage_tries }.into()
711+
Self { account_nodes, storage_tries }
699712
}
700713
}
701714

0 commit comments

Comments
 (0)