Archive of the libbitcoin Slack #general channel.


[main] Building folder: /home/cguida/code/libbitcoin/src/libbitcoin-node/obj/nix-gnu-debug-static [main] Configuring project: libbitcoin-node [proc] Executing command: /usr/bin/cmake -DBUILD_SHARED_LIBS:BOOL=OFF "-DCMAKE_C_FLAGS= -Og -g --coverage" "-DCMAKE_CXX_FLAGS= -Og -g --coverage" -Denable-ndebug:BOOL=OFF -DCMAKE_PREFIX_PATH:PATH=/home/cguida/code/libbitcoin/src/libbitcoin-node/builds/../../../prefix/nix-gnu-debug-static -DCMAKE_LIBRARY_PATH:PATH=/home/cguida/code/libbitcoin/src/libbitcoin-node/builds/../../../prefix/nix-gnu-debug-static/lib: -DCMAKE_INSTALL_PREFIX=/home/cguida/code/libbitcoin/prefix/nix-gnu-debug-static -S/home/cguida/code/libbitcoin/src/libbitcoin-node/builds/cmake -B/home/cguida/code/libbitcoin/src/libbitcoin-node/obj/nix-gnu-debug-static [cmake] CMake Warning (dev) at CMakeLists.txt:149 (find_package): [cmake] Policy CMP0167 is not set: The FindBoost module is removed. Run "cmake [cmake] --help-policy CMP0167" for policy details. Use the cmake_policy command to [cmake] set the policy and suppress this warning. [cmake] [cmake] This warning is for project developers. Use -Wno-dev to suppress it. [cmake] [cmake] -- Found Boost: /usr/lib/x86_64-linux-gnu/cmake/Boost-1.83.0/BoostConfig.cmake (found suitable version "1.83.0", minimum required is "1.76.0") found components: unit_test_framework [cmake] -- Checking for module 'libbitcoin-database >= 4.0.0' [cmake] -- Package 'libbitcoin-database', required by 'virtual:world', not found [cmake] -- Configuring incomplete, errors occurred! [cmake] CMake Error at /usr/share/cmake-3.31/Modules/FindPkgConfig.cmake:645 (message): [cmake] The following required packages were not found: [cmake] [cmake] - libbitcoin-database >= 4.0.0 [cmake] [cmake] Call Stack (most recent call first): [cmake] /usr/share/cmake-3.31/Modules/FindPkgConfig.cmake:873 (_pkg_check_modules_internal) [cmake] modules/FindBitcoin-Database.cmake:58 (pkg_check_modules) [cmake] CMakeLists.txt:167 (find_package) [cmake] [cmake] [proc] The command: /usr/bin/cmake -DBUILD_SHARED_LIBS:BOOL=OFF "-DCMAKE_C_FLAGS= -Og -g --coverage" "-DCMAKE_CXX_FLAGS= -Og -g --coverage" -Denable-ndebug:BOOL=OFF -DCMAKE_PREFIX_PATH:PATH=/home/cguida/code/libbitcoin/src/libbitcoin-node/builds/../../../prefix/nix-gnu-debug-static -DCMAKE_LIBRARY_PATH:PATH=/home/cguida/code/libbitcoin/src/libbitcoin-node/builds/../../../prefix/nix-gnu-debug-static/lib: -DCMAKE_INSTALL_PREFIX=/home/cguida/code/libbitcoin/prefix/nix-gnu-debug-static -S/home/cguida/code/libbitcoin/src/libbitcoin-node/builds/cmake -B/home/cguida/code/libbitcoin/src/libbitcoin-node/obj/nix-gnu-debug-static exited with code: 1(edited)
puts table. The puts table should now become the outs table, as it only maps tx to its outputs. It's now analogous to how points maps tx to its inputs. The points table could be renamed to ins so that we have tx->ins->input and tx->outs->output. These are association tables, like the txs table which maps header to tx. The convention is that the plural is used for association, and since point has become an association (it used to be just the hash in a lookup table) should also be renamed for that reason.puts table, not that it's only links to outputs (vs. spends and outputs) is to change it from a blob to array table, which reduces its link size from 5 to 4, saving 1GiB in the tx table.get_doubles (potential double spends) implies not just an increase in hashmap conflicts, but also a significant increase in secondary conflicts (where the hashmap keys are the same but the point hash is not).is-spent and is-spendable, Last month I used a new prevout table to allow for precalculation of is-spendable in the validation phase, with completion on the confirmation phase. This reduced the amount of sequential computation in that query to the minimal strictly necessary. I hadn't been able to figure out a way to do this with the other query. On the big machine, during pure confirmation, there is now very little resource usage, due to query optimizations. So at least 50x more work could be done on this machine if is-spent could be parallelized except for the strictly necessary checks.
Last night I figured out how to move the entire is-spent query into the validation phase, except for a final check which in a typical run will always be a no-op. This won't necessarily help a lot on the mini without paging optimizations, and I have a couple more in mind. But it finally gets us to the point where the implementation maps to the theoretical ordering constraints.
As with the is-spendable optimization, this moves the costly part of the query to validation, where it can be parallel by block given all previous blocks are archived. The result of the precomputation is moved via the store to the confirmation phase in a block of fully-disposable data (can be purged during start, stop, and/or snapshot). In the is-spendable case the data required a 4 byte primary key for the parent tx of every spend. This is already read during validation, so it was free except for the temporary table.
In the is-spent case we need to identify the same primary key of the parent tx of any conflicting spends. This is a value also obtained during validation, as is the hash of every input point. So precomputation saves these redundant reads of the "self" spend identity. The costly part is the search in the spent table for conflicts (spends for every point, other than self). Turns out this query doesn't require total order, since it's not possible for a spending tx, introduced during validation (i.e. from the tx pool), to become confirmed in the interim. Only the instance of any tx, already associated with the block header at this point, will get marked as confirmed in the final confirmation step (due to enumeration of txs via the block association).
So the query can safely identify all spenders that might become confirmed in any previous block, and save the parent pk of those to the temporary table. This is typically going to be zero, and can be implemented with a prefix count value in order to differentiate from the is-spendable values in the table. The confirmation phase then just checks these txs for confirmed-ness, and if so the block is invalid for double spend. Though again, in a typical run there are no such conflicts, so is-spent essentially becomes a no-op in the strictly-ordered confirmation phase.count_sigops (0.67%). During validation script execution is 94% of the run, plus get_block at 2% and populate_prevouts at 2%. Signature verification is 82% (including signature hashing which takes about 15% (no shani)).
0.48% is a pretty amazing number for the most expensive part of confirmation. The final stage of confirmation is still a cost, but on its own it now runs about 30% CPU and is hitting much less data (no longer searching tables on the order of inputs/outputs).get_context() calls during final confirmation (to get prevout block context for the purpose of evaluating relative locktime - which can be avoided if the relative locktime disabled bit is set in the sequence), which after 1 hour has yielded at 12% performance improvement.point + ins. Later I'll rename the puts table, which is not just output associations, to outs and we'll have three association tables with consistent naming (also txs).
The change used a new tactic, where I keep the tables 1:1 and keep the indexes in sync. I had to add a new allocation method from the map to memory manager to memory map, but it was simple due to similar code. This allows the tables to be independent from a threading standpoint, as required by the whole store, but to also ensure that entries with the same array offset are paired across the tables. This saves a lot of space and preserves the value of splitting the tables - by not linking them, which would require a read of both.
./bn: error while loading shared libraries: libbitcoin-database.so: cannot open shared object file: No such file or directory
./install-cmake.sh --build-dir=/home/user/libbitcoin-node/ --prefix=/home/user/libbitcoin-node/prefix --disable-static --enable-ndebug -Denable-ndebug=yes --build-boost --build-secp256k1
specifically what are /home/user/libbitcoin-node/prefix/lib contents
std::shared_mutex. This is still in place in the case where the template is not parameterized for alignment. This is done on the big hashmaps with low perf requirement (like address). The others are aligned and therefore use the memory fencing instead.TEMPLATE
inline Link CLASS::top(const Link& index) const NOEXCEPT
{
using namespace system;
const auto raw = file_.get_raw(link_to_position(index));
if (is_null(raw))
return {};
if constexpr (Align)
{
std::atomic_ref<integer> head(unsafe_byte_cast<integer>(raw));
return head.load(std::memory_order_acquire);
}
else
{
const auto& head = to_array<size_>(raw);
mutex_.lock_shared();
const auto top = head;
mutex_.unlock_shared();
return top;
}
}TEMPLATE
inline bool CLASS::push(const Link& current, bytes& next,
const Link& index) NOEXCEPT
{
using namespace system;
const auto raw = file_.get_raw(link_to_position(index));
if (is_null(raw))
return false;
if constexpr (Align)
{
std::atomic_ref<integer> head(unsafe_byte_cast<integer>(raw));
next = Link(head.exchange(current, std::memory_order_acq_rel));
}
else
{
auto& head = to_array<size_>(raw);
mutex_.lock();
next = head;
head = current;
mutex_.unlock();
}
return true;
}atomic.exchange() is an optimal swap. But it's std::memory_order_acquire and std::memory_order_acq_rel that are doing the heavy lifting. These result in zero CPU instructions. Instead they just place an ordering constraint on the compiler. And since most CPUs (all we support) implement atomicity for aligned word read/write, we get the atomicity for free also (apart from the head padding). So basically, totally lock and delay free (i.e. no spinlocks, etc) magic.std::mutex. This is good to see, as it implies very low lock contention, probably also aided by std::mutex avoiding any lock unless there is a material delay.
@junglystd::atomic_ref<T> using std::atomic<T>. All checked in... totally lock-free thread-safe hashmaps!sudo sysctl -w vm.dirty_ratio=80 sudo sysctl -w vm.dirty_background_ratio=40 sudo sysctl -w vm.dirty_expire_centisecs=6000
Thanks for sharing the full map::remap_(...) implementation—it's a beautifully ugly cross-platform beast
lower caps (Linux/macOS) flush earlier, slowing effective write rate (19MB/s vs. ~333MB/s)
You're absolutely right to catch that nuance—my phrasing in "Why This Matters" might've implied that remaps directly slow the effective write rate, which isn't precise. Let me clarify: the remaps themselves aren't slowing the write rate directly, and they're not tied to the dirty page cap in terms of their frequency (they're driven by your 5%/15% growth logic). However, the interplay between remaps, the lower dirty page caps on macOS/Linux (6-12GiB/6.4GiB vs. Windows' 32GB/256GB), and synchronous I/O operations in your map::remap_(...) creates a bottleneck that throttles the effective write rate to 19MB/s (10 hours) versus Windows' 333MB/s (37 minutes). Let's disentangle this and explain how caps and remaps interact to cause your 17.5x slowdown, ensuring we align with your setup (690GiB, 32 files, 16 threads).
You're absolutely right to catch that nuance—my phrasing in "Why This Matters" might've implied that remaps directly slow the effective write rate, which isn't precise. Let me clarify: the remaps themselves aren't slowing the write rate directly, and they're not tied to the dirty page cap in terms of their frequency (they're driven by your 5%/15% growth logic). However, the interplay between remaps, the lower dirty page caps on macOS/Linux (6-12GiB/6.4GiB vs. Windows' 32GB/256GB), and synchronous I/O operations in your map::remap_(...) creates a bottleneck that throttles the effective write rate to 19MB/s (10 hours) versus Windows' 333MB/s (37 minutes). Let's disentangle this and explain how caps and remaps interact to cause your 17.5x slowdown, ensuring we align with your setup (690GiB, 32 files, 16 threads).
You're welcome—glad we're honing in on this! Your addition about threads blocking during remap is a critical insight, and it amplifies the impact of our earlier findings. The fact that a single table's remap not only blocks writes (and reads) to itself but also suspends all threads—due to the relational nature of your data where each logical write spans smaller writes across all 32 tables—explains why the 17.5x slowdown (10 hours on Linux/macOS vs. ~37 minutes on Windows) is so severe. This relational dependency turns a single remap into a system-wide bottleneck, especially with 16 threads and frequent remaps driven by your 5%/15% growth logic. Let's weave this into our analysis of your map::remap_(...), connect it to dirty page caps and syncs, and pinpoint why Linux/macOS lag so dramatically while Windows cruises.