Skip to content

Conversation

martinus
Copy link
Contributor

@martinus martinus commented Nov 27, 2023

TL;DR: this change improves sync time on my PC by ~6%.

While syncing when the CCoinsViewCache gets full it is flushed to disk and then memory is freed. Depending on available cache size, this happens several times. This PR removes the unnecessary reallocations for temporary CCoinsViewCache and reserves the cacheCoins's bucket array to its previous size.

I benchmarked the change on an AMD Ryzen 9 7950X with this command:

hyperfine \
--show-output --parameter-list commit b5a271334ca81a6adcb1c608d85c83621a9eae47,ceeb71816512642e92a110b2cf5d2549d090fe78 \
--setup 'git checkout {commit} && make -j$(nproc) src/bitcoind' \
--prepare 'sync; sudo /sbin/sysctl vm.drop_caches=3' \
-M 3 './src/bitcoind -dbcache=500 -reindex-chainstate -printtoconsole=0 -stopatheight=500000'

Where b5a2713 is mergebase, and ceeb718 this PR.

Which runs ./src/bitcoind -dbcache=500 -reindex-chainstate -printtoconsole=0 -stopatheight=500000 3 times and compares both commits, giving this result:

Benchmark 1: ./src/bitcoind -dbcache=500 -reindex-chainstate -printtoconsole=0 -stopatheight=500000 (commit = b5a271334ca81a6adcb1c608d85c83621a9eae47)
  Time (mean ± σ):     2815.648 s ± 70.720 s    [User: 2639.959 s, System: 640.051 s]
  Range (min … max):   2736.616 s … 2872.964 s    3 runs

Benchmark 2: ./src/bitcoind -dbcache=500 -reindex-chainstate -printtoconsole=0 -stopatheight=500000 (commit = ceeb71816512642e92a110b2cf5d2549d090fe78)
  Time (mean ± σ):     2661.520 s ± 23.205 s    [User: 2473.010 s, System: 624.040 s]
  Range (min … max):   2635.816 s … 2680.926 s    3 runs

Summary
  ./src/bitcoind -dbcache=500 -reindex-chainstate -printtoconsole=0 -stopatheight=500000 (commit = ceeb71816512642e92a110b2cf5d2549d090fe78) ran
    1.06 ± 0.03 times faster than ./src/bitcoind -dbcache=500 -reindex-chainstate -printtoconsole=0 -stopatheight=500000 (commit = b5a271334ca81a6adcb1c608d85c83621a9eae47)

The short lived CCoinsViewCache's are destructed right after the Flush(), therefore it is not necessary to call ReallocateCache.
@DrahtBot
Copy link
Contributor

DrahtBot commented Nov 27, 2023

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

Code Coverage

For detailed information about the code coverage, see the test coverage report.

Reviews

See the guideline for information on the review process.
A summary of reviews will appear here.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #29236 (log: Nuke error(...) by maflcko)
  • #28280 (Don't empty dbcache on prune flushes: >30% faster IBD by andrewtoth)

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.

@helpau
Copy link

helpau commented Dec 9, 2023

Hi, is this benchmark for HDD or SSD? Did you tried to run benchmark with huge dbcache and high height?

@martinus
Copy link
Contributor Author

martinus commented Dec 9, 2023

I only measured it on an SSD, and only with relatively low dbcache size so flushing is triggered more often which I think would give more accurate results. But more benchmarks are definitely appreciated :)


// After a flush all the cacheCoins's memory has now been freed. It is likely that the map gets full again so flush is necessary, and it will
// happen around the same memory usage. So we can already reserve the bucket array to the previous size, reducing the number of rehashes needed.
cacheCoins.reserve(current_bucket_count);
Copy link
Contributor

@andrewtoth andrewtoth Dec 16, 2023

Choose a reason for hiding this comment

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

The purpose of ReallocateCache originally was to balance the snapshot and background chainstate caches (see #18637). However, in 5e4ac5a this function was removed from ResizeCoinsCaches and moved to Flush. This created an implicit assumption that calling Flush would clear the memory in the cache. This change violates that assumption, since the memory will no longer be clear and therefore ResizeCoinsCaches will likely no longer respect the dbcache limit during assumeutxo syncing.

Also see the docs for this function https://github.com/bitcoin/bitcoin/blob/master/src/coins.h#L336-L341. The purpose of this is to clear the memory of the cache.

Perhaps we could get the bucket count in Flush before calling ReallocateCache and reserve it in Flush, then re-add ReallocateCache to ResizeCoinsCaches?

edit: I see that reserve only allocates memory for the hashtable, and not the actual nodes stored in the unordered_map. I'm not sure how significant that extra memory is.

@@ -250,14 +250,16 @@ bool CCoinsViewCache::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlockIn
return true;
}

bool CCoinsViewCache::Flush() {
bool CCoinsViewCache::Flush(bool reallocate_cache) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this parameter be added to the fuzz targets or unit tests?

@andrewtoth
Copy link
Contributor

Ran same hyperfine benchmark with base, 985ddd9, and head of branch. Got similar results 🚀.

Summary
  ./src/bitcoind -dbcache=500 -reindex-chainstate -printtoconsole=0 -stopatheight=500000 (commit = ceeb71816512642e92a110b2cf5d2549d090fe78) ran
    1.06 ± 0.00 times faster than ./src/bitcoind -dbcache=500 -reindex-chainstate -printtoconsole=0 -stopatheight=500000 (commit = 985ddd954199cb7686ac75d12492d25b16d235c8)
    1.07 ± 0.00 times faster than ./src/bitcoind -dbcache=500 -reindex-chainstate -printtoconsole=0 -stopatheight=500000 (commit = b5a271334ca81a6adcb1c608d85c83621a9eae47)

It's unclear to me though why 5e4ac5a was necessary at all, and if reverting that change would be a cleaner fix for this instead?
From the commit message:

This is necessary in preparation for using the PoolAllocator for
CCoinsMap, which does not actually free any memory on clear().

Why is it necessary to free memory on clear()? Is the unfreed memory not just recycled for new coins as they are added? The only purpose of ReallocateCache seemed to be to clear all memory when rebalancing caches during assumeutxo syncing, and is unrelated to Flush.

@DrahtBot
Copy link
Contributor

🐙 This pull request conflicts with the target branch and needs rebase.

@achow101 achow101 requested a review from fjahr April 9, 2024 14:19
Copy link
Contributor

@fjahr fjahr left a comment

Choose a reason for hiding this comment

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

@martinus Are you still working on this? If yes, it would be great if you could rebase and address @andrewtoth 's comment. If not, I can try to take this over and get it the attention it deserves.

@@ -307,7 +307,7 @@ class CCoinsViewCache : public CCoinsViewBacked
* to be forgotten.
* If false is returned, the state of this cache (and its backing view) will be undefined.
*/
bool Flush();
bool Flush(bool reallocate_cache = true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should probably add something about this to the docstring.

@martinus
Copy link
Contributor Author

@fjahr I'm not working on this any more, feel free to take over!

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.

6 participants