Skip to content

Commit

Permalink
Use NodePoolResource for CCoinsMap
Browse files Browse the repository at this point in the history
In my benchmarks, using this pool allocator for CCoinsMap gives about
20% faster `-reindex-chainstate` with -dbcache=5000 with practically the
same memory usage. The change in max RSS changed was 0.3%.

Note that the memory usage behavior is now different, since NodePoolResource
does not free any memory when the map is cleared. Instead, the memory is
held in freelists. This requires now that `CCoinsViewCache::Flush`
needs to call `ReallocateCache` so memory is actually freed. If that
wouldn't happen, memory usage would not decrease and Flush() would be
triggered again soon after.

Also, the `validation_flush_tests` tests need to be updated because
memory allocation is now done in large pools instead of one node at a
time, so the limits need to be updated accordingly.
  • Loading branch information
martinus committed Jun 11, 2022
1 parent 973bf30 commit ba38016
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 19 deletions.
4 changes: 3 additions & 1 deletion src/coins.cpp
Expand Up @@ -275,7 +275,9 @@ void CCoinsViewCache::ReallocateCache()
// Cache should be empty when we're calling this.
assert(cacheCoins.size() == 0);
cacheCoins.~CCoinsMap();
::new (&cacheCoins) CCoinsMap();
cacheCoinsMemoryResource.~CCoinsMapMemoryResource();
::new (&cacheCoinsMemoryResource) CCoinsMapMemoryResource{};
::new (&cacheCoins) CCoinsMap{0, CCoinsMap::hasher{}, CCoinsMap::key_equal{}, &cacheCoinsMemoryResource};
}

static const size_t MIN_TRANSACTION_OUTPUT_WEIGHT = WITNESS_SCALE_FACTOR * ::GetSerializeSize(CTxOut(), PROTOCOL_VERSION);
Expand Down
14 changes: 12 additions & 2 deletions src/coins.h
Expand Up @@ -11,6 +11,7 @@
#include <memusage.h>
#include <primitives/transaction.h>
#include <serialize.h>
#include <support/allocators/nodepoolresource.h>
#include <uint256.h>
#include <util/hasher.h>

Expand Down Expand Up @@ -131,7 +132,15 @@ struct CCoinsCacheEntry
CCoinsCacheEntry(Coin&& coin_, unsigned char flag) : coin(std::move(coin_)), flags(flag) {}
};

typedef std::unordered_map<COutPoint, CCoinsCacheEntry, SaltedOutpointHasher> CCoinsMap;
using CCoinsMap = std::unordered_map<COutPoint,
CCoinsCacheEntry,
SaltedOutpointHasher,
std::equal_to<COutPoint>,
NodePoolAllocator<std::pair<const COutPoint, CCoinsCacheEntry>,
sizeof(std::pair<const COutPoint, CCoinsCacheEntry>) + sizeof(void*) * 4,
alignof(void*)>>;

using CCoinsMapMemoryResource = CCoinsMap::allocator_type::ResourceType;

/** Cursor for iterating over CoinsView state */
class CCoinsViewCursor
Expand Down Expand Up @@ -218,7 +227,8 @@ class CCoinsViewCache : public CCoinsViewBacked
* declared as "const".
*/
mutable uint256 hashBlock;
mutable CCoinsMap cacheCoins;
mutable CCoinsMapMemoryResource cacheCoinsMemoryResource{};
mutable CCoinsMap cacheCoins{0, CCoinsMap::hasher{}, CCoinsMap::key_equal{}, &cacheCoinsMemoryResource};

/* Cached dynamic memory usage for the inner Coin objects. */
mutable size_t cachedCoinsUsage;
Expand Down
29 changes: 28 additions & 1 deletion src/test/coins_tests.cpp
Expand Up @@ -6,6 +6,7 @@
#include <coins.h>
#include <script/standard.h>
#include <streams.h>
#include <test/util/nodepoolresourcetester.h>
#include <test/util/setup_common.h>
#include <txdb.h>
#include <uint256.h>
Expand Down Expand Up @@ -608,7 +609,8 @@ void GetCoinsMapEntry(const CCoinsMap& map, CAmount& value, char& flags)

void WriteCoinsViewEntry(CCoinsView& view, CAmount value, char flags)
{
CCoinsMap map;
CCoinsMapMemoryResource resource;
CCoinsMap map{0, CCoinsMap::hasher{}, CCoinsMap::key_equal{}, &resource};
InsertCoinsMapEntry(map, value, flags);
BOOST_CHECK(view.BatchWrite(map, {}));
}
Expand Down Expand Up @@ -877,4 +879,29 @@ BOOST_AUTO_TEST_CASE(ccoins_write)
CheckWriteCoins(parent_value, child_value, parent_value, parent_flags, child_flags, parent_flags);
}

BOOST_AUTO_TEST_CASE(coins_resource_is_used)
{
CCoinsMapMemoryResource resource;
NodePoolResourceTester::CheckAllDataAccountedFor(resource);

{
CCoinsMap map{0, CCoinsMap::hasher{}, CCoinsMap::key_equal{}, &resource};
BOOST_TEST(memusage::DynamicUsage(map) >= resource.ChunkSizeBytes());

map.reserve(1000);

// The resource has preallocated a chunk, so we should have space for at several nodes without the need to allocate anything else.
const auto usage_before = memusage::DynamicUsage(map);

COutPoint out_point{};
for (size_t i = 0; i < 1000; ++i) {
out_point.n = i;
map[out_point];
}
BOOST_TEST(usage_before == memusage::DynamicUsage(map));
}

NodePoolResourceTester::CheckAllDataAccountedFor(resource);
}

BOOST_AUTO_TEST_SUITE_END()
3 changes: 2 additions & 1 deletion src/test/fuzz/coins_view.cpp
Expand Up @@ -112,7 +112,8 @@ FUZZ_TARGET_INIT(coins_view, initialize_coins_view)
random_mutable_transaction = *opt_mutable_transaction;
},
[&] {
CCoinsMap coins_map;
CCoinsMapMemoryResource resource;
CCoinsMap coins_map{0, CCoinsMap::hasher{}, CCoinsMap::key_equal{}, &resource};
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) {
CCoinsCacheEntry coins_cache_entry;
coins_cache_entry.flags = fuzzed_data_provider.ConsumeIntegral<unsigned char>();
Expand Down
36 changes: 22 additions & 14 deletions src/test/validation_flush_tests.cpp
Expand Up @@ -54,12 +54,12 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate)
BOOST_TEST_MESSAGE("CCoinsViewCache memory usage: " << view.DynamicMemoryUsage());
};

constexpr size_t MAX_COINS_CACHE_BYTES = 1024;
// NodePoolResource defaults to 256 KiB that will be allocated, so we'll take that and make it a bit larger.
constexpr size_t MAX_COINS_CACHE_BYTES = (1 << 18) + 512;

// Without any coins in the cache, we shouldn't need to flush.
BOOST_CHECK_EQUAL(
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, /*max_mempool_size_bytes=*/0),
CoinsCacheSizeState::OK);
BOOST_TEST(
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, /*max_mempool_size_bytes=*/ 0) != CoinsCacheSizeState::CRITICAL);

// If the initial memory allocations of cacheCoins don't match these common
// cases, we can't really continue to make assertions about memory usage.
Expand Down Expand Up @@ -89,17 +89,25 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate)
// cacheCoins (unordered_map) preallocates.
constexpr int COINS_UNTIL_CRITICAL{3};

// no coin added, so we have plenty of space left.
BOOST_CHECK_EQUAL(
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, /*max_mempool_size_bytes*/ 0),
CoinsCacheSizeState::OK);

for (int i{0}; i < COINS_UNTIL_CRITICAL; ++i) {
COutPoint res = add_coin(view);
print_view_mem_usage(view);
BOOST_CHECK_EQUAL(view.AccessCoin(res).DynamicMemoryUsage(), COIN_SIZE);

// adding first coin causes the MemoryResource to allocate one 256 KiB chunk of memory,
// pushing us immediately over to LARGE
BOOST_CHECK_EQUAL(
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, /*max_mempool_size_bytes=*/0),
CoinsCacheSizeState::OK);
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, /*max_mempool_size_bytes=*/ 0),
CoinsCacheSizeState::LARGE);
}

// Adding some additional coins will push us over the edge to CRITICAL.
for (int i{0}; i < 4; ++i) {
for (int i{0}; i < 50; ++i) {
add_coin(view);
print_view_mem_usage(view);
if (chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, /*max_mempool_size_bytes=*/0) ==
Expand All @@ -112,16 +120,16 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate)
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, /*max_mempool_size_bytes=*/0),
CoinsCacheSizeState::CRITICAL);

// Passing non-zero max mempool usage should allow us more headroom.
// Passing non-zero max mempool usage (512 KiB) should allow us more headroom.
BOOST_CHECK_EQUAL(
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, /*max_mempool_size_bytes=*/1 << 10),
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, /*max_mempool_size_bytes=*/ 1 << 19),
CoinsCacheSizeState::OK);

for (int i{0}; i < 3; ++i) {
add_coin(view);
print_view_mem_usage(view);
BOOST_CHECK_EQUAL(
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, /*max_mempool_size_bytes=*/1 << 10),
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, /*max_mempool_size_bytes=*/ 1 << 19),
CoinsCacheSizeState::OK);
}

Expand All @@ -137,7 +145,7 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate)
BOOST_CHECK(usage_percentage >= 0.9);
BOOST_CHECK(usage_percentage < 1);
BOOST_CHECK_EQUAL(
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, 1 << 10),
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, /*max_mempool_size_bytes*/ 1 << 10), // 1024
CoinsCacheSizeState::LARGE);
}

Expand All @@ -149,8 +157,8 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate)
CoinsCacheSizeState::OK);
}

// Flushing the view doesn't take us back to OK because cacheCoins has
// preallocated memory that doesn't get reclaimed even after flush.
// Flushing the view does take us back to OK because cacheCoinsMemoryResource frees its
// preallocated memory

BOOST_CHECK_EQUAL(
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, 0),
Expand All @@ -162,7 +170,7 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate)

BOOST_CHECK_EQUAL(
chainstate.GetCoinsCacheSizeState(MAX_COINS_CACHE_BYTES, 0),
CoinsCacheSizeState::CRITICAL);
CoinsCacheSizeState::OK);
}

BOOST_AUTO_TEST_SUITE_END()

0 comments on commit ba38016

Please sign in to comment.