-
Notifications
You must be signed in to change notification settings - Fork 38.7k
Change UpdateForDescendants to use Epochs #18191
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
Change UpdateForDescendants to use Epochs #18191
Conversation
cae5e32 to
182e676
Compare
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsNo conflicts as of last run. |
sdaftuar
left a comment
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.
Only looked at ec237cf8c640f9e85c3c7aad0d3eb243a346b1f9 but I think maybe not worth the added complexity, if I'm understanding correctly.
Also the shrink_to_fit of the update_cache vector could just be included in #18120?
80a2488 to
ae89491
Compare
|
Fixed and updated comment. |
857c42d to
b262e8c
Compare
|
Sorry for all the line noise; I realized I forgot to add a continue, and then decided that I could DRY up the code a bit. I'm pretty happy with the final version, I think it's a bit easier to view because we don't treat update_it special from other iterators we walk (other than it not going into the cache line). I can squash this all down into one commit and replace #18120. |
b262e8c to
32985d3
Compare
|
Concept ACK. |
|
IIUC, the first pass of the
I'd suggest to remove it completely.
I'd suggest:
EDIT: I realized, that you mean "children" and "grandchildren" relative to an element of the |
hebasto
left a comment
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.
8098dea test: Add mempool_updatefromblock.py (Hennadii Stepanov) Pull request description: This PR adds a new test for mempool update of transaction descendants/ancestors information (count, size) when transactions have been re-added from a disconnected block to the mempool. It could be helpful for working on PRs like #17925, #18191. ACKs for top commit: ariard: ACK 8098dea Tree-SHA512: 7e808fa8df8d7d7a7dbdc3f79361049b49c7bce9b58fd5539b28c9636bedac747695537e500d7ed68dc8bdb80167ad3f1c01086f7551691405d2ba2e38ef1d06
8098dea test: Add mempool_updatefromblock.py (Hennadii Stepanov) Pull request description: This PR adds a new test for mempool update of transaction descendants/ancestors information (count, size) when transactions have been re-added from a disconnected block to the mempool. It could be helpful for working on PRs like bitcoin#17925, bitcoin#18191. ACKs for top commit: ariard: ACK 8098dea Tree-SHA512: 7e808fa8df8d7d7a7dbdc3f79361049b49c7bce9b58fd5539b28c9636bedac747695537e500d7ed68dc8bdb80167ad3f1c01086f7551691405d2ba2e38ef1d06
|
It seems that performance is the motivation for this PR. Could benchmarks, or a link to them, be added to the PR description? |
|
For what it's worth, 30 test runs on master versus this branch rebased on master seem to show no significant difference in run time (~0.5%). They do appear to show a significant reduction in run time variance with this PR. I ran four alternating cycles of 15: master, PR, master, PR; it took most of a day to for the 60 runs. results with a 2012 MacBookPro running Mojave
|
|
The build warnings I encountered building in Debian with Clang 6 were also present when building on MacOS with the default Apple clang version 11. In both cases, these are the only compiler warnings I'm seeing while building Bitcoin Core. They can be removed by either making the txiters const reference or non-const. txmempool.cpp:89:35: warning: loop variable 'grand_child_it' of type 'const CTxMemPool::txiter' (aka 'const hashed_index_iterator<hashed_index_node<ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, index_node_base<CTxMemPoolEntry, std::__1::allocator<CTxMemPoolEntry> > > > >, boost::multi_index::detail::hashed_unique_tag>, bucket_array<allocator<CTxMemPoolEntry> >, boost::multi_index::detail::hashed_index_global_iterator_tag>') creates a copy from type 'const CTxMemPool::txiter' [-Wrange-loop-analysis]
for (const txiter grand_child_it : cached_grand_children->second) {
^
txmempool.cpp:89:22: note: use reference type 'const CTxMemPool::txiter &' (aka 'const hashed_index_iterator<hashed_index_node<ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, index_node_base<CTxMemPoolEntry, std::__1::allocator<CTxMemPoolEntry> > > > >, boost::multi_index::detail::hashed_unique_tag>, bucket_array<allocator<CTxMemPoolEntry> >, boost::multi_index::detail::hashed_index_global_iterator_tag> &') to prevent copying
for (const txiter grand_child_it : cached_grand_children->second) {
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
txmempool.cpp:78:27: warning: loop variable 'child_it' of type 'const CTxMemPool::txiter' (aka 'const hashed_index_iterator<hashed_index_node<ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, index_node_base<CTxMemPoolEntry, std::__1::allocator<CTxMemPoolEntry> > > > >, boost::multi_index::detail::hashed_unique_tag>, bucket_array<allocator<CTxMemPoolEntry> >, boost::multi_index::detail::hashed_index_global_iterator_tag>') creates a copy from type 'const CTxMemPool::txiter' [-Wrange-loop-analysis]
for (const txiter child_it : GetMemPoolChildren(next_it)) {
^
txmempool.cpp:78:14: note: use reference type 'const CTxMemPool::txiter &' (aka 'const hashed_index_iterator<hashed_index_node<ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, index_node_base<CTxMemPoolEntry, std::__1::allocator<CTxMemPoolEntry> > > > >, boost::multi_index::detail::hashed_unique_tag>, bucket_array<allocator<CTxMemPoolEntry> >, boost::multi_index::detail::hashed_index_global_iterator_tag> &') to prevent copying
for (const txiter child_it : GetMemPoolChildren(next_it)) {
^~~~~~~~~~~~~~~~~~~~~~~
2 warnings generated. |
I think it would help the PR to feature relevant benchmarkings, with instructions to reproduce them, in the PR description. |
|
I don't think we have documentation on how to track memory usage. I remember I used massif at one point, but I forgot to write down notes. #17164 (comment) |
Good point. Generally this one is supposed to be an obviously better version that doesn't really require much benching. You can easily see from the code that std::sets go to vectors and a std::set of all conflicts is removed. But it's a good standard to hold to I suppose to want benches/testing for this stuff even if it seems obvious -- but as @MarcoFalke points out, benching memory is a bit tricky... @sdaftuar do you think any of the benches you've run previously can be published in a reproducible way? |
|
@jonatack I'll fix the iterator reference thing. Is there a way to make this warning trigger on all platforms that you know about? |
|
I encountered identical build warnings with MacOS clang 10. |
|
Concept ACK, also encountered the build warnings though. |
|
Will fix the build warnings soon; thanks all for the review (will tag some people when I try to fix as I can't trigger locally). |
32985d3 to
8e5aa08
Compare
|
@jonatack can you confirm this fixes the build warning you encountered? |
|
Confirming that 32985d34a0a0cfd4ff8c4d96ff461778eaf72a8a produced the warnings but 8e5aa08 does not. |
hebasto
left a comment
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.
re-ACK 8e5aa08, tested on Linux Mint 19.3 (x86_64) with Clang 6.0.0:
- 32985d34a0a0cfd4ff8c4d96ff461778eaf72a8a
$ make > /dev/null
...
txmempool.cpp:89:35: warning: loop variable 'grand_child_it' of type 'const CTxMemPool::txiter' (aka 'const hashed_index_iterator<hashed_index_node<ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, index_node_base<CTxMemPoolEntry, std::allocator<CTxMemPoolEntry> > > > >, boost::multi_index::detail::hashed_unique_tag>, bucket_array<allocator<CTxMemPoolEntry> >, boost::multi_index::detail::hashed_index_global_iterator_tag>') creates a copy from type 'const CTxMemPool::txiter' [-Wrange-loop-analysis]
for (const txiter grand_child_it : cached_grand_children->second) {
^
txmempool.cpp:89:22: note: use reference type 'const CTxMemPool::txiter &' (aka 'const hashed_index_iterator<hashed_index_node<ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, index_node_base<CTxMemPoolEntry, std::allocator<CTxMemPoolEntry> > > > >, boost::multi_index::detail::hashed_unique_tag>, bucket_array<allocator<CTxMemPoolEntry> >, boost::multi_index::detail::hashed_index_global_iterator_tag> &') to prevent copying
for (const txiter grand_child_it : cached_grand_children->second) {
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
txmempool.cpp:78:27: warning: loop variable 'child_it' of type 'const CTxMemPool::txiter' (aka 'const hashed_index_iterator<hashed_index_node<ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, index_node_base<CTxMemPoolEntry, std::allocator<CTxMemPoolEntry> > > > >, boost::multi_index::detail::hashed_unique_tag>, bucket_array<allocator<CTxMemPoolEntry> >, boost::multi_index::detail::hashed_index_global_iterator_tag>') creates a copy from type 'const CTxMemPool::txiter' [-Wrange-loop-analysis]
for (const txiter child_it : GetMemPoolChildren(next_it)) {
^
txmempool.cpp:78:14: note: use reference type 'const CTxMemPool::txiter &' (aka 'const hashed_index_iterator<hashed_index_node<ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, ordered_index_node<boost::multi_index::detail::null_augment_policy, index_node_base<CTxMemPoolEntry, std::allocator<CTxMemPoolEntry> > > > >, boost::multi_index::detail::hashed_unique_tag>, bucket_array<allocator<CTxMemPoolEntry> >, boost::multi_index::detail::hashed_index_global_iterator_tag> &') to prevent copying
for (const txiter child_it : GetMemPoolChildren(next_it)) {
^~~~~~~~~~~~~~~~~~~~~~~
2 warnings generated.
...
- 8e5aa08 -- no such warnings
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 think this PR should layout more optimization goal, i.e are these performance changes pursued to improve adversarial scenarios where a burdensome to evaluate reorg would unbalance block race ? Or it's still under the whole Epoch mempool goal to support more complex packages ?
You should mention performance improvements in commit message, namely adding an epoch on descendants traversal to avoid duplicate and swap-in std::vector against std::set to remove the O(log n) at element insertion and argue why that's an improvement. Wrt to std::vector, is this also an improvement memory-size ? You can also spread them on 2 different commits to ease review.
Overall, maybe you should come with a benchmark suite with package payloads of different complexity to avoid hitting hidden performance regressions ? I don't think mempool_updatefromblock has been designed for this.
| // but don't traverse again. | ||
| for (txiter cacheEntry : cacheIt->second) { | ||
| setAllDescendants.insert(cacheEntry); | ||
| const auto epoch = GetFreshEpoch(); |
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 think you should layout high-level algorithm behavior here, like "cacheMap maintain a key-elements mapping where each key represents a back-to-the-mempool transactions, elements represents its children sorted XXX. We merge cache line based on key intersecting with children of a latter cache entry evaluation"
You can keep per-line comments but it would be better to normalize style, IMO you blur code behavior description (e.g "Create something") and assumptions ("Elements are excluded, ...") which is a bit confusing.
| int64_t modify_count = 0; | ||
| // All entries in the new_cache_line have | ||
| // already been checked against excluded list | ||
| for (txiter child_it : new_cache_line) { |
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 wonder if previous accounting logic was correct to exclude already-processed vHashesToUpdate member from descendant state update. E.g if back-to-mempool transaction Y is processed first, as reverse_iterate enforces, its parents X, also to process as second, won't account it as part of modify_fee and others.
AFAICT, new code maintains behavior by not including Y as part of X's children cache line.
Correct me if I miss some boost:multi_index internals ?
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.
Ok so suppose vhashestoupdate is {X, Y}.
First we submit X to mempool and Y to mempool.
Then we reverse iterate and update Y, then X.
Previously, we scanned across all of the direct children in the mempool of Y then of X and updated the parent/child relationships. So Y would do only things already in the mempool, and X would see Y in this list, but exclude it in setExcluded as it was set properly when Y was put in the mempool.
In the new patch, we scanned across all of the direct children in the mempool of Y then of X and updated the parent/child relationships. So Y would do only things already in the mempool, and X would see Y in this list, but exclude it in mapMemPoolDescendants as it was set properly when Y was put in the mempool, and mapMemPoolDescendants will get a cacheline entry for everything in vHashesToUpdate; and because of iteration order, children will always be visible to parents.
When we actually call into UpdateForDescendants, we only update against things that are in the cachelines, and not the keys to those cachelines. This is because earlier when Y was accepted to the mempool it should have already modified X's fees.
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.
Gotcha, thanks for the laid-out explanation, I forgot that ancestor state X is well-modified at Y new mempool insertion. I was just confused by the sybillic comment "because any descendants in cache were added to the mempool after the transaction being updated and hence their state is already reflected in the parent state". In fact that's should point to AcceptToMemoryPool in validation's UpdateMempoolForReorg to enhance clarity.
|
🐙 This pull request conflicts with the target branch and needs rebase. Want to unsubscribe from rebase notifications on this pull request? Just convert this pull request to a "draft". |
There hasn't been much activity lately and the patch still needs rebase. What is the status here?
|
| // We can skip updating entries we've encountered before or that | ||
| // are in the block (which are already accounted for). | ||
| if (!visited(childIter) && !setAlreadyIncluded.count(childHash)) { | ||
| if (!visited(childIter) && !mapMemPoolDescendantsToUpdate.count(childIter)) { |
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.
Not sure if this PR is still active, but I believe it may be more efficient to check:
+++ if (!visited(childIter) && !childIter->GetMemPoolParentsConst().count(it) {since if the child is unvisited and is in vHashesToUpdate, it will have it as a parent, which isn't the case otherwise. I think this would be more efficient if the childiter's set of parents is < cache size, which I would expect to be true more often than not
|
this is a duplicate of #24158 and has needed rebase for 18 months, close? |
I think so. Looking at the comments in #24158:
|
8098dea test: Add mempool_updatefromblock.py (Hennadii Stepanov) Pull request description: This PR adds a new test for mempool update of transaction descendants/ancestors information (count, size) when transactions have been re-added from a disconnected block to the mempool. It could be helpful for working on PRs like bitcoin#17925, bitcoin#18191. ACKs for top commit: ariard: ACK 8098dea Tree-SHA512: 7e808fa8df8d7d7a7dbdc3f79361049b49c7bce9b58fd5539b28c9636bedac747695537e500d7ed68dc8bdb80167ad3f1c01086f7551691405d2ba2e38ef1d06
This patch replaces #18120, fixing some regressions or issues with that PR. The overall change here should be easier to review as well.
Refactors UpdateForDescendants to use Epochs instead of sets to de-duplicate traversal, and replaces the cache entries with a vector instead of a set. This is a straightforward win. The algorithm is a bit clever so as not to require two separate vectors for txiters that need expanding and txiters that have already been processed, but I think it's relatively easy to reason about.
This is extracted from #18063 as a standalone because it turns out the DoS issues with this code perhaps merit more extensive review http://gnusha.org/bitcoin-core-dev/2020-02-11.log. That PR will stay open for review, but this PR is designed as an "obviously better" win that can be merged now to keep progress up on the mempool project.
sdaftuar can you re-run your bench from #18120?