-
Notifications
You must be signed in to change notification settings - Fork 376
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
Optimize AccountIndexIterator to reuse last loaded bin-map item range #4729
base: master
Are you sure you want to change the base?
Conversation
1b9589c
to
4ed42d0
Compare
4ed42d0
to
1bfd02e
Compare
I have been running this PR for the past 3 days on mainnet with out any issues. |
1bfd02e
to
0fe2226
Compare
I pushed a commit to fix the conflicts after rebasing to master. |
I kinda feel that the sorted vs unsorted iterators are fundamentally different enough where we could (should?) have two separate iterator types and implementations. Wdyt? |
yes. i think it is good idea. |
be2f7b3
to
33a74c6
Compare
56078ca
to
9cfe2ab
Compare
9cfe2ab
to
7ae7553
Compare
@brooksprumo can you help to review this PR again? |
Sure, will do! |
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.
I need to go through the core of the implementation still. There's some code organization that is making the review a bit challenging for me in Github. I'll pull the code locally and see if that helps.
@brooksprumo I pushed a commit to implement sorted/unsorted iterator in a different way. Can you take a look again? thanks! |
I like this direction. Can we go one step further and remove |
ok. pushed another change to remove the trait and unshare the common code between two types of iterators. let me know wdyt. |
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.
Wow, ok, I've spent a lot of time staring at all this. Sorry for all the refactor/nit questions/comments. Let me know your thoughts on how to proceed. Some things of the code currently in master feel broken and I'm not sure if/when/where to fix it all.
accounts-db/src/accounts_index.rs
Outdated
fn bin_start_and_range(&self) -> (usize, usize) { | ||
let start_bin = self.start_bin(); | ||
// calculate the max range of bins to look in | ||
let end_bin_inclusive = self.end_bin_inclusive(); | ||
let bin_range = if start_bin > end_bin_inclusive { | ||
0 // empty range | ||
} else if end_bin_inclusive == usize::MAX { | ||
usize::MAX | ||
} else { | ||
// the range is end_inclusive + 1 - start | ||
// end_inclusive could be usize::MAX already if no bound was specified | ||
end_bin_inclusive.saturating_add(1) - start_bin | ||
}; | ||
(start_bin, bin_range) | ||
} |
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.
This impl looks identical to me between Sorted and Unsorted. Can this be refactored into a bare function that takes start_bin
and end_bin_inclusive
as parameters?
Then the next()
impls can call the bare function directly.
(We'll also need to do this for hold_range_in_memory()
, but that's fine too. This function probably should not be in the iterator interface at all. We can refactor that in a subsequent PR, or here.
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.
Exactly, that's why I tried to introduce the inner class / the trait to share all these functions.
Should we revert this commit and go back with the trait implementation?
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.
done.
- moved bin_start_and_range to free fn.
- inlined fn range to next.
- move hold_range_in_memory to AccountsIndex.
accounts-db/src/accounts_index.rs
Outdated
fn start_bin(&self) -> usize { | ||
// start in bin where 'start_bound' would exist | ||
bin_from_bound(self.bin_calculator, &self.start_bound, 0) | ||
} | ||
|
||
fn end_bin_inclusive(&self) -> usize { | ||
// end in bin where 'end_bound' would exist | ||
bin_from_bound(self.bin_calculator, &self.end_bound, usize::MAX) | ||
} |
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.
nit: Can the ordering of the methods be the same in Unsorted and Sorted, please? This makes it easier to compare the code for both side by side.
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.
done.
accounts-db/src/accounts_index.rs
Outdated
fn range<R>(map: &AccountMaps<T, U>, range: R) -> RangeItemVec<T> | ||
where | ||
R: RangeBounds<Pubkey> + std::fmt::Debug, | ||
{ | ||
let mut result = map.items(&range); | ||
result.sort_unstable_by(|a, b| a.0.cmp(&b.0)); | ||
result | ||
} |
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.
Feels like range()
here can go too (i.e. be inlined into new()
). We only call it in one place, and it doesn't seem all that useful on its own. This is even more true for the impl on Unsorted.
(And then can the RangeItemVec alias be removed too, please?)
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.
done.
But we still need RangeItemVec. it is used in last_bin_range to make clippy happy.
error: very complex type used. Consider factoring parts into `type` definitions
--> accounts-db/src/accounts_index.rs:591:21
|
591 | last_bin_range: Option<(usize, Vec<(Pubkey, AccountMapEntry<T>)>)>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity
= note: `-D clippy::type-complexity` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::type_complexity)]`
let mut range = Self::range(&map, (self.start_bound, self.end_bound)); | ||
chunk.append(&mut range); | ||
} | ||
self.is_finished = true; |
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.
Lol, the more I look at the original code the more confused I am. This Unsorted iterator doesn't actually iterate at all... it just returns all the items in the range, and does not respect ITER_BATCH_SIZE
at all. There will only ever be a single "iteration" of this iterator.
We probably want the unsorted iterator to respect the batch size though? That or we don't call this thing an iterator... Ugh, that'd be a lot of change that is not really related to this PR. I'm not sure what to do: (1) fix the unsorted iterator in master first, (2) leave the unsorted iterator unchanged in this pr, or (3) fix it within this PR. I'm inclined to think (1) or (2), but not (3). Wdyt?
We may want to put back in the
self.start_bound = Excluded(chunk.last().unwrap().0);
line at the end of the function though. If we do want to respect the batch size in the future, that line will be required.
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.
I prefer 2 to keep the unsorted behavior the same.
We are not seeing any issue with unsorted iter yet. We may want to keep it as it is.
accounts-db/src/accounts_index.rs
Outdated
let iter: Box<dyn Iterator<Item = Vec<(Pubkey, AccountMapEntry<T>)>>> = | ||
if returns_items == AccountsIndexIteratorReturnsItems::Sorted { | ||
Box::new(self.iter_sorted(range.as_ref())) | ||
} else { | ||
Box::new(self.iter_unsorted(range.as_ref())) | ||
}; |
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.
Ugh... crap... right. If we don't use an enum on the two iter impls, then we need dynamic dispatch, or we need to make a fn around the for loop below with an impl Iterator
parameter.
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.
Oh neat, the Either
crate handles this!
https://docs.rs/either/latest/either/enum.Either.html#method.iter
(not saying we need to use either
, just sharing)
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.
yeah. that's why i used enum for static dispatch in the first place.
But I think it shouldn't matter much for performance to use the dynamic dispatch here.
For unsorted iter, we only call next once. For sorted iter, we call next every 1000 elements.
we should be fine to use dyn dispatch here.
pushed one more commit to save two copies of pubkey in hold_range_in memory. After inlining the hold_range_in memory, we already have a ref of Range. so we can use |
one more change on the iterator to take ref of pubkey range instead of copy. |
Problem
AccountIndexIterator fetches the items from accounts map in batches, when it is configured "sorted". For each batch, it fetch the result from the underlying bin map by loading, filtering and sorting. However, when a batch only partially consumes a bin map, next batch will start loading/filtering/sorting the same bin map from the beginning again.
Imagining that a bin map contains 10K items, each batch size is 1K, then when we return 10 batches, we will be loading/filtering/sorting the bin-map for 10 times. There is a lot of waste.
We should optimize this by caching the bin-map loaded result in the iterator, so that we only load/filter/sort each bin map at most once.
Summary of Changes
Optimize AccountIndexIterator to reuse last loaded bin-map range.
Also, refactor AccountIndexInterator into two separate iterators - sorted and unsorted. This specialization will improve the IPC of the core loop for both types of iterators.
Fixes #