Skip to content

Conversation

@glozow
Copy link
Member

@glozow glozow commented Oct 1, 2021

Remove the txmempool <-> validation circular dependency by removing txmempool's dependency on validation. There are two functions in txmempool that need validation right now: check() and removeForReorg(). This PR removes the dependencies in check().

This PR also improves the performance of CTxMemPool::check() by walking through the entries exactly once, in ascending ancestorcount order, which guarantees that we see parents before children.

@glozow
Copy link
Member Author

glozow commented Oct 1, 2021

bench results on master:

ns/op op/s err% total benchmark
8.34 119,851,301.59 0.0% 0.01 MempoolCheck

results on top of this PR:

ns/op op/s err% total benchmark
2.13 468,844,203.61 0.0% 0.01 MempoolCheck

(~4x faster)

@flack
Copy link
Contributor

flack commented Oct 1, 2021

Sorry for being off-topic, but just because I always twitch slightly when I see it: The bench output is already formatted in Markdown extended syntax, so if you just paste it into the comment box, without backticks around it (and without leading whitespace), it renders a bit nicer. And it's easier to do, because less steps :-)

ns/op op/s err% total benchmark
8.34 119,851,301.59 0.0% 0.01 MempoolCheck
ns/op op/s err% total benchmark
2.13 468,844,203.61 0.0% 0.01 MempoolCheck

@DrahtBot
Copy link
Contributor

DrahtBot commented Oct 1, 2021

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #23173 (Add ChainstateManager::ProcessTransaction by jnewbery)
  • #22677 (cut the validation <-> txmempool circular dependency 2/2 by glozow)
  • #21527 (net_processing: lock clean up by ajtowns)
  • #17786 (refactor: Nuke policy/fees->mempool circular dependencies by hebasto)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

Copy link
Contributor

@theStack theStack left a comment

Choose a reason for hiding this comment

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

Concept ACK

Copy link
Contributor

@jnewbery jnewbery left a comment

Choose a reason for hiding this comment

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

utACK 4bc0c8066e79f0d95053bcc2449a7d354eaa9c25

I've left a few nitty style comments inline.

I found the ordering of the commits:

  • [mempool] speed up check by using coins cache and iterating in topo order
  • [mempool] assert that check() always goes in topological order
  • [mempool] remove code needed for unsorted mempool checking

to be a little bit confusing. In the intermediate commits, you have things like:

                assert(mempoolDuplicate.HaveCoin(txin.prevout));
                if (!mempoolDuplicate.HaveCoin(txin.prevout)) fDependsWait = true;

which you then remove in the subsequent commit. It's fine, but it's a bit more mental overhead for reviewers.

I think a more intuitive progression of commits would be to squash those three commits into two:

  • the first commit just changes the for loop to use GetSortedDepthAndScore() and adds the prev_ancestor_count sanity check.
  • the second commit removes the waitingOnDependants code since the iteration is now in depth order.

Is the next step here to change check() to take a const CCoinsViewCache& and int64_t height and remove the dependency on validation entirely?

@glozow glozow force-pushed the 2021-09-speed-mempool-check branch from 4bc0c80 to a420bcb Compare October 4, 2021 12:31
@glozow
Copy link
Member Author

glozow commented Oct 4, 2021

@flack good tip, fixed

@theStack @jnewbery thanks for your reviews, I think I've incorporated all suggestions :)

Is the next step here to change check() to take a const CCoinsViewCache& and int64_t height and remove the dependency on validation entirely?

Yes I thought I had included that commit here but apparently I hadn't - last push added it.

@glozow glozow force-pushed the 2021-09-speed-mempool-check branch from a420bcb to 6bb6ee4 Compare October 4, 2021 12:50
@jnewbery
Copy link
Contributor

jnewbery commented Oct 4, 2021

Is the next step here to change check() to take a const CCoinsViewCache& and int64_t height and remove the dependency on validation entirely?

Yes I thought I had included that commit here but apparently I hadn't - last push added it.

It might actually be better to defer that commit until after #23173 so fewer changes are needed to net_processing. Entirely up to you.

glozow added 7 commits October 4, 2021 15:00
… order

No behavior changes.

Before, we're always adding transactions to the "check later" queue if
they have any parents in the mempool. But there's no reason to do this
if all of its inputs are already available from mempoolDuplicate.
Instead, check for inputs, and only mark fDependsWait=true if the
parents haven't been processed yet.

Reduce the amount of "check later" transactions by looking at
ancestors before descendants. Do this by iterating through them in
ascending order by ancestor count. This works because a child will
always have more in-mempool ancestors than its parent.

We should never have any entries in the "check later" queue
after this commit.
Remove variables used for keeping track of mempool transactions for
which we haven't processed the parents yet. Since we're iterating in
topological order now, they're always unused.
UpdateCoins is an unnecessary dependency on validation. All we need to
do is add and remove coins to check inputs. We don't need the extra
logic for checking coinbases and handling TxUndos.

Also remove the wrapper function in validation.h which constructs a
throwaway TxUndo object before calling UpdateCoins because it is now
unused.
No transaction in the mempool should ever be a coinbase.

Since mempoolDuplicate's backend is the chainstate coins view, it should
always contain the coins available.
Removes check's dependency on validation.h
@jnewbery
Copy link
Contributor

jnewbery commented Oct 4, 2021

reACK 082c5bf

Comment on lines +85 to +88
int childTxs = 800;
if (bench.complexityN() > 1) {
childTxs = static_cast<int>(bench.complexityN());
}
Copy link

@SachinMeier SachinMeier Oct 6, 2021

Choose a reason for hiding this comment

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

Nit: This could be simplified to:

int childTxs = bench.complexityN() > 1 ? static_cast<int>(bench.complexityN()) : 800;

as it is on line 105

@GeneFerneau
Copy link

tACK 082c5bf

Ran the bench a few times before and after the optimization:

Before optimization refactor

ns/op op/s err% total benchmark
81.26 12,306,038.69 0.0% 0.01 MempoolCheck
81.26 12,305,850.81 0.0% 0.01 MempoolCheck
81.26 12,306,043.84 0.0% 0.01 MempoolCheck
12,305,977.78 Average

After optimization refactor

ns/op op/s err% total benchmark
10.72 93,324,244.07 0.0% 0.01 MempoolCheck
10.72 93,323,975.05 0.0% 0.01 MempoolCheck
10.72 93,323,120.41 0.0% 0.01 MempoolCheck
93,323,779.84 Average

Optimization increase in op/s

93,323,779.84 / 12,305,977.78 = 7.5836 (758.36%)

@LarryRuane
Copy link
Contributor

concept ACK
I'll try to do a code review, but FWIW, my working clone is usually built with optimization disabled (-O0) to make debugging easier, and there's an even bigger improvement that way (approximately 18x):

before:

ns/op op/s err% total benchmark
215.53 4,639,632.68 2.1% 0.01 MempoolCheck

after:

ns/op op/s err% total benchmark
12.12 82,495,736.15 3.8% 0.01 MempoolCheck

Copy link
Contributor

@rajarshimaitra rajarshimaitra left a comment

Choose a reason for hiding this comment

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

tACK 082c5bf

Bench comparison

Optimized

ns/op op/s err% total benchmark
2.39 417,806,513.20 0.6% 0.01 MempoolCheck
2.39 419,189,093.49 0.6% 0.01 MempoolCheck
2.39 419,188,854.66 0.6% 0.01 MempoolCheck
2.39 419,049,672.97 0.3% 0.01 MempoolCheck

Non-Optimized

ns/op op/s err% total benchmark
12.61 79,317,536.11 6.8% 0.01 MempoolCheck
12.20 81,995,745.32 9.2% 0.01 MempoolCheck
12.22 81,862,095.21 1.9% 0.01 MempoolCheck
12.17 82,167,397.00 0.8% 0.01 MempoolCheck

It seems I am observing only 6x speedup. But that's probably my local benching issue.

Copy link
Contributor

@theStack theStack left a comment

Choose a reason for hiding this comment

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

Not sure what's different or wrong on my system (running OpenBSD 6.9/amd64), but the benchmark doesn't show any noticable difference after the optimization in CTxMemPool::check, it even seems to be worse.

at commit 30e240f (with original check as on master branch):

$ ./src/bench/bench_bitcoin -filter=MempoolCheck
ns/op op/s err% total benchmark
100.66 9,934,356.36 1.6% 0.01 MempoolCheck

at commit 54c6f3c (with optimized check):

$ ./src/bench/bench_bitcoin -filter=MempoolCheck
ns/op op/s err% total benchmark
107.36 9,314,108.15 4.4% 0.01 MempoolCheck

Will try on a Debian Linux machine later to compare.

// EDIT: It seems like the difference in benchmark results is almost only caused by the very last commit.

@glozow
Copy link
Member Author

glozow commented Oct 20, 2021

It seems like the difference in benchmark results is almost only caused by the very last commit.

That sounds strange to me, but I guess it's plausible that the bench only sees a speedup because of the parameter change rather than ordered traversal. Even so, I'd want to remove all that unnecessary code...

@theStack
Copy link
Contributor

That sounds strange to me, but I guess it's plausible that the bench only sees a speedup because of the parameter change rather than ordered traversal. Even so, I'd want to remove all that unnecessary code...

Agree, removing unnecessary code is always a good thing! I didn't intend to sound concept NACKy, this was just a hint that the posted benchmark results have to be taken with a grain of salt :) will likely code-review this PR within the next days.

Copy link
Contributor

@theStack theStack left a comment

Choose a reason for hiding this comment

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

Code-review ACK 082c5bf

@maflcko maflcko merged commit 1847ce2 into bitcoin:master Oct 25, 2021
@glozow glozow deleted the 2021-09-speed-mempool-check branch October 25, 2021 13:26
sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Oct 25, 2021
Copy link
Contributor

@mjdietzx mjdietzx left a comment

Choose a reason for hiding this comment

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

Post merge ACK 082c5bf

Warming up to review some of your open PRs. Just one question / possible nit. You are KILLING it with these PRs, well done 💪

assert(it->GetSizeWithAncestors() == nSizeCheck);
assert(it->GetSigOpCostWithAncestors() == nSigOpCheck);
assert(it->GetModFeesWithAncestors() == nFeesCheck);
// Sanity check: we are walking in ascending ancestor count order.
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this sanity check necessary? It's not like GetSortedDepthAndScore() is complicated or has non-deterministic behavior, right?

And why do we really need to fail if somehow we do encounter this case? The only down-side I see is marking this as a "check later" transaction which is just a minor performance hit?

Are there any race conditions where we could possibly see this assert throw?

Copy link
Contributor

Choose a reason for hiding this comment

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

Alright, I'm reviewing commit by commit, and now in e8639ec I am starting to understand why we have this assert.

But, I'm still wondering, why have this assert if assert(mempoolDuplicate.HaveCoin(txin.prevout)); will take care of the job should we run into any problems? I still don't see why this is beneficial

Copy link
Member Author

Choose a reason for hiding this comment

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

It's not strictly necessary, no. It's just a relatively cheap-to-run sanity check. It can make things easier to debug if we have any problems, but yes it can be removed and this code would still be checking the same things.

Fabcien pushed a commit to Bitcoin-ABC/bitcoin-abc that referenced this pull request Oct 7, 2022
Summary:
This is a backport of [[bitcoin/bitcoin#23157 | core#23157]] [1 & 2/8]
bitcoin/bitcoin@cb14071
bitcoin/bitcoin@30e240f

Test Plan:
`ninja bench-bitcoin`

|               ns/op |                op/s |    err% |     total | benchmark
|--------------------|--------------------|--------|----------|----------
|      172,478,220.00 |                5.80 |    0.1% |      1.90 | `ComplexMemPool`
|                 ... |                 ... |     ... |       ... | ...
|                8.07 |      123,977,164.61 |    0.0% |      0.00 | `MempoolCheck`

Reviewers: #bitcoin_abc, Fabien

Reviewed By: #bitcoin_abc, Fabien

Subscribers: Fabien

Differential Revision: https://reviews.bitcoinabc.org/D12169
Fabcien pushed a commit to Bitcoin-ABC/bitcoin-abc that referenced this pull request Oct 7, 2022
Summary:
> [mempool] speed up check() by using coins cache and iterating in topo order
>
> No behavior changes.
>
> Before, we're always adding transactions to the "check later" queue if
> they have any parents in the mempool. But there's no reason to do this
> if all of its inputs are already available from mempoolDuplicate.
> Instead, check for inputs, and only mark fDependsWait=true if the
> parents haven't been processed yet.
>
> Reduce the amount of "check later" transactions by looking at
> ancestors before descendants. Do this by iterating through them in
> ascending order by ancestor count. This works because a child will
> always have more in-mempool ancestors than its parent.
>
> We should never have any entries in the "check later" queue
> after this commit.

bitcoin/bitcoin@54c6f3c

> [mempool] remove now-unnecessary code
>
> Remove variables used for keeping track of mempool transactions for
> which we haven't processed the parents yet. Since we're iterating in
> topological order now, they're always unused.

bitcoin/bitcoin@e8639ec

This is a backport of [[bitcoin/bitcoin#23157 | core#23157]] [3 & 4/8]

Depends on D12169

Test Plan:
`ninja all check-all bench-bitcoin`

Note that the benchmarks do not show any speed-up after this commit. The only significant change happensafter the last commit of the stack, when the `ActiveChainState()` pointer dereference is moved out of the benchmarked code.

I ran this in between the commits as well, to exercise all the assertions.

Reviewers: #bitcoin_abc, Fabien

Reviewed By: #bitcoin_abc, Fabien

Differential Revision: https://reviews.bitcoinabc.org/D12170
Fabcien pushed a commit to Bitcoin-ABC/bitcoin-abc that referenced this pull request Oct 7, 2022
Summary:
UpdateCoins is an unnecessary dependency on validation. All we need to
do is add and remove coins to check inputs. We don't need the extra
logic for checking coinbases and handling TxUndos.

Also remove the wrapper function in validation.h which constructs a
throwaway TxUndo object before calling UpdateCoins because it is now
unused.

This is a backport of [[bitcoin/bitcoin#23157 | core#23157]] [6/8]
bitcoin/bitcoin@9e8d7ad

Notes:
- there was another occurence of the removed `UpdateCoins` function in the unit test added in rABC880d847f076f7b38c849ce3f31120f2f582a0ba0 . For that case, as the tx is a coinbase transaction (without inputs), `UpdateCoins` was just calling `AddCoins`.
- the removed `UpdateCoins` function was slighlty modified in D1479  to avoid building an uneccessary `CTxUndo`

Depends on D12171

Test Plan: `ninja all check-all`

Reviewers: #bitcoin_abc, Fabien

Reviewed By: #bitcoin_abc, Fabien

Differential Revision: https://reviews.bitcoinabc.org/D12172
Fabcien pushed a commit to Bitcoin-ABC/bitcoin-abc that referenced this pull request Oct 7, 2022
Summary:
No transaction in the mempool should ever be a coinbase.

Since mempoolDuplicate's backend is the chainstate coins view, it should
always contain the coins available.

This is a backport of [[bitcoin/bitcoin#23157 | core#23157]] [7/8]
bitcoin/bitcoin@ed6115f

Depends on D12172

Test Plan: `ninja all check-all`

Reviewers: #bitcoin_abc, Fabien

Reviewed By: #bitcoin_abc, Fabien

Differential Revision: https://reviews.bitcoinabc.org/D12173
Fabcien pushed a commit to Bitcoin-ABC/bitcoin-abc that referenced this pull request Oct 7, 2022
Summary:
Removes check's dependency on validation.h. This is a step towards removing the `txmempool --> validation` circular dependency.

From the PR's description:
> There are two functions in txmempool that need validation right now: check() and removeForReorg(). This PR removes the dependencies in check().

This concludes backport of [[bitcoin/bitcoin#23157 | core#23157]] [8/8]
bitcoin/bitcoin@082c5bf#diff-6875de769e90cec84d2e8a9c1b962cdbcda44d870d42e4215827e599e11e90e3

Depends on D12173

The fuzzer change is not applicable due to missing backports.

Test Plan: `ninja all check-all bench-bitcoin`

Reviewers: #bitcoin_abc, Fabien

Reviewed By: #bitcoin_abc, Fabien

Subscribers: Fabien

Differential Revision: https://reviews.bitcoinabc.org/D12174
@bitcoin bitcoin locked and limited conversation to collaborators Oct 30, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.