Using less memory than allowed decreases the coins cache hit ratio (the
fraction of lookups that find the UTXO in the cache); using more memory
than specified risks crashing bitcoind on memory-restricted systems.
For this reason, it’s important to keep an accurate accounting of the amount
of memory used by the cache. It doesn’t have to be perfect but should be
When a program requests X bytes of dynamic memory from the C++ runtime
library, internally it allocates slightly more for the memory allocator’s
metadata (overhead). In other words, logical memory is not the same
as physical memory.
This memory allocator metadata is somewhat complex and depends on several
factors, such as the machine architecture and the
When sizing the cache, we want to calculate physical memory usage, that is,
account for this extra allocation metadata. Unfortunately, there’s no
library function that maps logical memory size to physical size.
To deal with that, Bitcoin Core includes a function,
that approximates this conversion. Its argument is an allocation size,
and it returns the corresponding physical size.
That source file, memusage.h,
includes many overloads of the function DynamicUsage() across
the various data types that we might be allocating somewhere
in the system. They all make use of MallocUsage().
The pool memory resource (added by PR #25325) adds a new
overload (version) that computes the overall coins cache size. This allows us to stay
within the configured cache size.
<LarryRuane> So, after writing today's notes and questions, it occurred to me that some here might not be familiar with lower-level structures like `unordered_map` in c++, especially if you've come from higher-level languages like Ruby or Python
<LarryRuane> So in bitcoin core, a super important data structure is the "coins cache" ... this is a map (we use the standard library `unordered_map`) that lets us determine if a transaction's input refers to a valid, unspent output
<LarryRuane> so, even though all the unspent outputs (UTXOs) are saved on disk (in the `chainstate` directory within your data directory, in LevelDB format), the most recent ones are saved in memory -- in this very unordered_map we're talking about today
<LarryRuane> we'd like this to happen as often as possible, because it makes IBD much faster than it would otherwise be, because reading from disk is like 2 orders of magnitude, or more, slower than just reading from memory
<LarryRuane> and also (I had to discover all this myself recently), the mempool cache is used for this map during IBD, and it has a separate config setting, `-maxmempool` i think, and its default is 300 MiB
<LarryRuane> and each entry contains (in this case of the coins map) the map key (a `COutpoint` like we described) and a `Coin` which is the amount of this output and its `scriptPubKey` .. did I miss anything, @stickies-v ?
<LarryRuane> so again, we're validating a tx... we loop across its inputs ... each one contains a `COutpoint` ... we look in the map using this `COutpoint` (which remember is a txid and output index) ...
<LarryRuane> I think this relates to what some of you may know much better than me, functional programming ... where data structures are much more often immutable, and you apply changes atomically once everything is validated or no errors, else discard
<LarryRuane> Okay so back to unordered_map ... it's faster than an (ordered) `std::map` ... the tradeoff being that a `std::map` allows you to iterate the entries in key-order, but at the expense of time and (maybe) storage
<LarryRuane> So anyway, an unordered_map has this bucket array, and that has to be accounted for when we try to figure out how much actual memory this map is consuming, and this (finally!) relates to this PR
<LarryRuane> but this layer in between (you can think of it as) makes the unordered_map's allocations more efficient in terms of both speed and (pyysical) memory usage, even though the unordered_map (which we don't control) is doing exactly the same memory allocations
<LarryRuane> but I think mainly what it did is allowed MORE coins entries to be cached in memory before we hit that physical memory limit, so there is a higher hit ratio (the percentage of times we look for a UTXO in memory and it's present, don't have to read disk)
<LarryRuane> Seems like the `std::unordered_map` implementation should use the custom memory allocator to allocate the bucket array... so then its physical memory usage would be accounted for in the "chunks" (see the line just above, like 185)
<LarryRuane> in this PR (that we're reviewing) we just keep track of any system allocations that WE (the custom allocator) do, and account for the bucket array that way, without ever having to call `m.bucket_count()`