-
Couldn't load subscription status.
- Fork 2k
perf(engine): return sorted data from compute_trie_input #19340
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
base: main
Are you sure you want to change the base?
Conversation
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
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.
| /// 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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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)> { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) = |
There was a problem hiding this comment.
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)> { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
Closes #19249
Eliminates sorting overhead per block by returning
TrieInputSortedinstead of unsortedTrieInputfromcompute_trie_input.Previously,
MultiProofConfig::from_input()would calldrain_into_sorted()on both nodes and state every block, performing expensive sorting operations:This PR introduced
TrieInputSortedtype that holds sorted data from the start.Now
compute_trie_inputsorts once at the end before returning, andMultiProofConfig::from_inputbecomes a simple Arc wrapper.