Skip to content

Conversation

@yongkangc
Copy link
Member

@yongkangc yongkangc commented Oct 28, 2025

Closes #19249

Eliminates sorting overhead per block by returning TrieInputSorted instead of unsorted TrieInput from compute_trie_input.

Previously, MultiProofConfig::from_input() would call drain_into_sorted() on both nodes and state every block, performing expensive sorting operations:

This PR introduced TrieInputSorted type that holds sorted data from the start.

Now compute_trie_input sorts once at the end before returning, and MultiProofConfig::from_input becomes a simple Arc wrapper.

This eliminates 2-5ms of sorting overhead per block by returning
TrieInputSorted instead of unsorted TrieInput from compute_trie_input.

Previously, MultiProofConfig::from_input would call drain_into_sorted()
on both nodes and state, performing expensive sorting operations every
block. Now compute_trie_input sorts once at the end and returns sorted
data, making MultiProofConfig::from_input a simple Arc wrapper.

Changes:
- Add TrieInputSorted type with sorted TrieUpdates and HashedPostState
- Add clear() methods to TrieUpdatesSorted and HashedPostStateSorted
- Update compute_trie_input to return (TrieInputSorted, BlockNumber)
- Update MultiProofConfig::from_input to accept TrieInputSorted
- Update BasicEngineValidator to store Option<TrieInputSorted>

The implementation uses a "build unsorted, sort once" strategy:
unsorted HashMap-based structures are used during building for fast
extend operations, then sorted once before returning. This eliminates
redundant sorting while maintaining performance.

Resolves: #19249
@yongkangc yongkangc added C-perf A change motivated by improving speed, memory usage or disk footprint A-engine Related to the engine implementation labels Oct 28, 2025
@yongkangc yongkangc requested a review from Rjected as a code owner October 28, 2025 04:15
@github-project-automation github-project-automation bot moved this to Backlog in Reth Tracker Oct 28, 2025
@yongkangc yongkangc marked this pull request as draft October 28, 2025 04:21
@yongkangc yongkangc self-assigned this Oct 28, 2025
@yongkangc yongkangc moved this from Backlog to In Progress in Reth Tracker Oct 28, 2025
Updated the handling of trie input in the compute_trie_input function to improve performance and memory efficiency. The changes include:

- Replaced the use of Option<TrieInputSorted> with Option<TrieInput> to allow for better reuse of allocated capacity.
- Introduced a new method, drain_into_sorted, in TrieInput to convert it into TrieInputSorted while retaining HashMap capacity for subsequent operations.
- Adjusted the logic in compute_trie_input to utilize the new method, reducing unnecessary allocations and improving performance during block validations.

These modifications streamline the trie input processing, enhancing overall efficiency in the engine's validation workflow.
@yongkangc yongkangc added the A-trie Related to Merkle Patricia Trie implementation label Oct 29, 2025
/// This type holds sorted versions of trie data structures, which eliminates the need
/// for expensive sorting operations during multiproof generation.
#[derive(Default, Debug, Clone)]
pub struct TrieInputSorted {
Copy link
Member Author

Choose a reason for hiding this comment

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

we should write some test for this

///
/// The sorted output allocates new `Vec` space, but the original `HashMap` capacity is
/// retained for the next cycle.
pub fn drain_into_sorted(&mut self) -> TrieInputSorted {
Copy link
Member Author

Choose a reason for hiding this comment

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

do we really need this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

imo it's not needed

StateRootStrategy::StateRootTask => {
// get allocated trie input if it exists
let allocated_trie_input = self.trie_input.take();
let allocated = self.trie_input.take();
Copy link
Member Author

Choose a reason for hiding this comment

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

lets not change the name for this

allocated_trie_input: Option<TrieInput>,
) -> ProviderResult<(TrieInput, BlockNumber)> {
// get allocated trie input or use a default trie input
) -> ProviderResult<(TrieInputSorted, TrieInput, BlockNumber)> {
Copy link
Member Author

Choose a reason for hiding this comment

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

why do we still need TrieInput? cant we remove that?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Definitely, yes, allocated_trie_input should be a TrieInputSorted

/// Append state to the input by reference and extend the prefix sets.
pub fn append_ref(&mut self, state: &HashedPostState) {
self.prefix_sets.extend(state.construct_prefix_sets());
let sorted_state = state.clone().into_sorted();
Copy link
Member Author

Choose a reason for hiding this comment

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

so basically we are applying sorting here, would that be a concern for performance regression?

Copy link
Collaborator

Choose a reason for hiding this comment

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

if we're not using this method in this PR then I don't think we should add it. This logic made sense on TrieInput when we were not able to revert trie data, but now it's not necessary I don't think

Replaced the existing HashedPostState and TrieUpdates with their sorted counterparts, HashedPostStateSorted and TrieUpdatesSorted, in the ExecutedBlock struct. This change enhances the efficiency of state handling by ensuring that the trie updates and hashed state are maintained in a sorted order, improving performance during block execution and validation.
- The previous approach:
  - Converts every sorted block back into hash maps (cloning all
    keys/values once per block) because extend_with_blocks works
    on the unsorted representation.
  - After all that, drain_into_sorted() iterates those hash maps,
    builds sorted Vecs, and drains the allocations—more cloning
    and shuffling before we return to the same sorted layout we
    could have maintained from the start.

- So the new loop cuts out the conversion overhead and reduces
  allocations; the old code was strictly more work for the same
  end result.
// forms of the state/trie fields.
let (trie_input, multiproof_config) = MultiProofConfig::from_input(trie_input);
self.trie_input.replace(trie_input);
let (mut cleared_sorted_input, multiproof_config) =
Copy link
Collaborator

Choose a reason for hiding this comment

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

The conversion to MultiProofConfig is no longer really necessary, you can use the trie_input fields directly when constructing the OverlaystateProviderFactory

allocated_trie_input: Option<TrieInput>,
) -> ProviderResult<(TrieInput, BlockNumber)> {
// get allocated trie input or use a default trie input
) -> ProviderResult<(TrieInputSorted, TrieInput, BlockNumber)> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Definitely, yes, allocated_trie_input should be a TrieInputSorted

///
/// The sorted output allocates new `Vec` space, but the original `HashMap` capacity is
/// retained for the next cycle.
pub fn drain_into_sorted(&mut self) -> TrieInputSorted {
Copy link
Collaborator

Choose a reason for hiding this comment

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

imo it's not needed

/// Append state to the input by reference and extend the prefix sets.
pub fn append_ref(&mut self, state: &HashedPostState) {
self.prefix_sets.extend(state.construct_prefix_sets());
let sorted_state = state.clone().into_sorted();
Copy link
Collaborator

Choose a reason for hiding this comment

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

if we're not using this method in this PR then I don't think we should add it. This logic made sense on TrieInput when we were not able to revert trie data, but now it's not necessary I don't think

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-engine Related to the engine implementation A-trie Related to Merkle Patricia Trie implementation C-perf A change motivated by improving speed, memory usage or disk footprint

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

Return sorted data from compute_trie_input

3 participants