[IBD] coins: increase default UTXO flush batch size to 32 MiB #31645

pull l0rinc wants to merge 2 commits into bitcoin:master from l0rinc:l0rinc/utxo-dump-batching changing 3 files +8 −8
  1. l0rinc commented at 8:08 pm on January 12, 2025: contributor

    This change is part of [IBD] - Tracking PR for speeding up Initial Block Download

    Summary

    When the in-memory UTXO set is flushed to LevelDB (after IBD or AssumeUTXO load), it does so in batches to manage memory usage during the flush. A hidden -dbbatchsize config option exists to modify this value. This PR only changes the default from 16 MiB to 32 MiB. Using a larger default reduces the overhead of many small writes and improves I/O efficiency (especially on HDDs). It may also help LevelDB optimize writes more effectively (e.g., via internal ordering).

    Context

    The UTXO set has grown significantly since 2017, when the original fixed 16 MiB batch size was chosen.

    With the current multi-gigabyte UTXO set and the common practice of using larger -dbcache values, the fixed 16 MiB batch size leads to several inefficiencies:

    • Flushing the entire UTXO set often requires thousands of separate 16 MiB write operations.
    • Particularly on HDDs, the cumulative disk seek time and per-operation overhead from numerous small writes significantly slow down the flushing process.
    • Each WriteBatch call incurs internal LevelDB overhead (e.g., MemTable handling, compaction triggering logic). More frequent, smaller batches amplify this cumulative overhead.

    Flush times of 20-30 minutes are not uncommon, even on capable hardware.

    Considerations

    As noted by sipa, flushing involves a temporary memory usage increase as the batch is prepared. A larger batch size naturally leads to a larger peak during this phase. Crashing due to OOM during a flush is highly undesirable, but now that #30611 is merged, the most we’d lose is the first hour of IBD.

    Increasing the LevelDB write batch size from 16 to 32 MiB raised the measured peaks by ~70 MiB in my tests during UTXO dump. The option remains hidden, and users can always override it.

    The increased peak memory usage (detailed below) is primarily attributed to LevelDB’s leveldb::Arena (backing MemTables) and the temporary storage of serialized batch data (e.g., std::string in CDBBatch::WriteImpl).

    Performance gains are most pronounced on systems with slower I/O (HDDs), but some SSDs also show measurable improvements.

    Measurements:

    AssumeUTXO proxy, multiple runs with error bars (flushing time is faster that the measured loading + flushing):

    • Raspberry Pi, dbcache=500: ~30% faster with 32 MiB vs 16 MiB, peak +~75 MiB and still < 1 GiB.
    • i7 + HDD: results vary by dbcache, but 32 MiB usually beats 16 MiB and tracks close to 64 MiB without the larger peak.
    • i9 + fast NVMe: roughly flat across 16/32/64 MiB. The goal here is to avoid regressions, which holds.

    Reproducer:

     0# Set up a clean demo environment
     1mkdir -p demo && rm -rfd demo
     2
     3# Build Bitcoin Core
     4cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j$(nproc)
     5
     6# Start bitcoind with minimal settings without mempool and internet connection
     7build/bin/bitcoind -datadir=demo -stopatheight=1
     8build/bin/bitcoind -datadir=demo -blocksonly=1 -connect=0 -dbcache=30000 -daemon
     9
    10# Load the AssumeUTXO snapshot, making sure the path is correct
    11# Expected output includes `"coins_loaded": 184821030`
    12build/bin/bitcoin-cli -datadir=demo -rpcclienttimeout=0 loadtxoutset ~/utxo-880000.dat
    13
    14# Stop the daemon and verify snapshot flushes in the logs
    15build/bin/bitcoin-cli -datadir=demo stop
    16grep "FlushSnapshotToDisk: completed" demo/debug.log
    

    This PR originally proposed 64 MiB, then a dynamic size, but both were dropped: 64 MiB increased peaks more than desired on low-RAM systems, and the dynamic variant underperformed across mixed hardware. 32 MiB is a simpler default that captures most of the gains with a modest peak increase.

    For more details see: #31645 (comment)

    While the PR isn’t about IBD in general, rather about a critical section of it, I have measured a reindex-chainstate until 900k blocks, showing a 1% overall speedup:

     0COMMITS="e6bfd95d5012fa1d91f83bf4122cb292afd6277f af653f321b135a59e38794b537737ed2f4a0040b"; \
     1STOP=900000; DBCACHE=10000; \
     2CC=gcc; CXX=g++; \
     3BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/BitcoinData"; LOG_DIR="$BASE_DIR/logs"; \
     4(echo ""; for c in $COMMITS; do git fetch -q origin $c && git log -1 --pretty='%h %s' $c || exit 1; done; echo "") && \
     5hyperfine \
     6  --sort command \
     7  --runs 1 \
     8  --export-json "$BASE_DIR/rdx-$(sed -E 's/(\w{8})\w+ ?/\1-/g;s/-$//'<<<"$COMMITS")-$STOP-$DBCACHE-$CC.json" \
     9  --parameter-list COMMIT ${COMMITS// /,} \
    10  --prepare "killall bitcoind 2>/dev/null; rm -f $DATA_DIR/debug.log; git checkout {COMMIT}; git clean -fxd; git reset --hard && \
    11    cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release && ninja -C build bitcoind && \
    12    ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=1000 -printtoconsole=0; sleep 10" \
    13  --cleanup "cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-$(date +%s).log" \
    14  "COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0"
    15
    16e6bfd95d50 Merge bitcoin-core/gui#881: Move `FreespaceChecker` class into its own module
    17af653f321b coins: derive `batch_write_bytes` from `-dbcache` when unspecified
    18
    19Benchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=900000 -dbcache=10000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = e6bfd95d5012fa1d91f83bf4122cb292afd6277f)
    20  Time (abs ):        25016.346 s               [User: 30333.911 s, System: 826.463 s]
    21 
    22Benchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=900000 -dbcache=10000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = af653f321b135a59e38794b537737ed2f4a0040b)
    23  Time (abs ):        24801.283 s               [User: 30328.665 s, System: 834.110 s]
    24 
    25Relative speed comparison
    26        1.01          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=900000 -dbcache=10000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = e6bfd95d5012fa1d91f83bf4122cb292afd6277f)
    27        1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=900000 -dbcache=10000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = af653f321b135a59e38794b537737ed2f4a0040b)
    
  2. l0rinc commented at 8:11 pm on January 12, 2025: contributor

    Visual representation of the AssumeUTXO 840k measurements (16MiB was the previous default, 64MiB is the proposed one):

     0set -e
     1
     2killall bitcoind 2>/dev/null || true
     3cd /mnt/my_storage/bitcoin
     4mkdir -p demo
     5
     6for i in 1 2; do
     7  for dbcache in 440 5000 30000; do
     8    for dbbatchsize in 4194304 8388608 16777216 33554432 67108864 134217728 268435456; do
     9      cmake -B build -DCMAKE_BUILD_TYPE=Release > /dev/null 2>&1
    10      cmake --build build -j"$(nproc)" > /dev/null 2>&1
    11
    12      build/bin/bitcoin-cli -datadir=demo stop 2>/dev/null || true
    13      killall bitcoind 2>/dev/null || true
    14      sleep 10
    15
    16      rm -rfd demo/chainstate demo/chainstate_snapshot
    17      build/bin/bitcoind -datadir=demo -stopatheight=1 -printtoconsole=0 && rm -f demo/debug.log
    18
    19      echo "Starting bitcoind with dbcache=$dbcache"
    20      build/bin/bitcoind -datadir=demo -blocksonly=1 -connect=0 -dbcache="$dbcache" -dbbatchsize="$dbbatchsize" -daemon -printtoconsole=0
    21      sleep 10
    22
    23      echo "Loading UTXO snapshot..."
    24      build/bin/bitcoin-cli -datadir=demo -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-880000.dat | grep -q '"coins_loaded": 184821030' || { echo "ERROR: Wrong number of coins loaded"; exit 1; }
    25      build/bin/bitcoin-cli -datadir=demo stop
    26      killall bitcoind 2>/dev/null || true
    27      sleep 10
    28
    29      out_file="results_i_${i}_dbcache_${dbcache}_dbbatchsize_${dbbatchsize}.log"
    30      echo "Collecting logs in ${out_file}"
    31      grep "FlushSnapshotToDisk: completed" demo/debug.log | tee -a "${out_file}"
    32      echo "---" >> "${out_file}"
    33
    34      echo "Done with i=${i}, dbcache=${dbcache}, dbbatchsize=${dbbatchsize}"
    35      echo
    36    done
    37  done
    38done
    39
    40echo "All runs complete. Logs are saved in results_dbcache*.log files."
    
     0import re
     1import sys
     2
     3
     4def parse_bitcoin_debug_log(file_path):
     5    results = []
     6
     7    flush_sum = 0.0
     8    flush_count = 0
     9
    10    version_pattern = re.compile(r"Bitcoin Core version")
    11    flush_pattern = re.compile(r'FlushSnapshotToDisk: completed \(([\d.]+)ms\)')
    12
    13    def finalize_current_block():
    14        nonlocal flush_sum, flush_count
    15        if flush_count > 0:
    16            results.append((flush_sum, flush_count))
    17        flush_sum = 0.0
    18        flush_count = 0
    19
    20    try:
    21        with open(file_path, 'r') as file:
    22            for line in file:
    23                if version_pattern.search(line):
    24                    finalize_current_block()
    25                    continue
    26
    27                match_flush = flush_pattern.search(line)
    28                if match_flush:
    29                    flush_ms = float(match_flush.group(1))
    30                    flush_sum += flush_ms
    31                    flush_count += 1
    32    except Exception as e:
    33        print(f"Error reading file: {e}")
    34        sys.exit(1)
    35
    36    finalize_current_block()
    37
    38    return results
    39
    40
    41if __name__ == "__main__":
    42    if len(sys.argv) < 2:
    43        print("Usage: python3 script.py <path_to_debug_log>")
    44        sys.exit(1)
    45
    46    file_path = sys.argv[1]
    47    parsed_results = parse_bitcoin_debug_log(file_path)
    48
    49    for total_flush_time, total_flush_calls in parsed_results:
    50        print(f"{total_flush_time:.2f},{total_flush_calls}")
    

    Edit: Reran the benchmarks on a Hetzner Linux machine with the new AssumeUTXO 880k set, parsing the logged flush times and plotting the results. For different dbcache (440, 5000, 30000) and dbbatchsize (4-256MiB range, 16MiB (current) and 64MiB (proposed) are highlighted and trendline is added for clarity), each one twice for stability:


    Sorting the same measurements (and calculating the sums) might give us a better understanding of the trends:

  3. DrahtBot commented at 8:11 pm on January 12, 2025: contributor

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

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31645.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Stale ACK ryanofsky, jonatack, hodlinator

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #32427 ((RFC) kernel: Replace leveldb-based BlockTreeDB with flat-file based store by TheCharlatan)

    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.

  4. laanwj added the label UTXO Db and Indexes on Jan 13, 2025
  5. sipa commented at 3:52 pm on January 13, 2025: member

    FWIW, the reason for the existence of the batch size behavior (as opposed to just writing everything at once) is that it causes a memory usage spike at flush time. If that spike exceeds the memory the process can allocate it causes a crash, at a particularly bad time (may require a replay to fix, which may be slower than just reprocessing the blocks).

    Given that changing this appears to improve performance it’s worth considering of course, but it is essentially a trade-off between speed and memory usage spiking.

  6. 1440000bytes commented at 6:00 pm on January 13, 2025: none

    If there are tradeoffs (speed, memory usage etc.) involved in changing default batch size, then it could remain the same.

    Maybe a config option can be provided to change it.

  7. sipa commented at 6:02 pm on January 13, 2025: member
    There is a config option. This is about changing the dedault.
  8. 1440000bytes commented at 6:12 pm on January 13, 2025: none

    There is a config option. This is about changing the dedault.

    Just realized dbbatchsize already exists.

  9. l0rinc commented at 6:58 pm on January 13, 2025: contributor

    If that spike exceeds the memory the process can allocate it causes a crash

    Thanks for the context, @sipa. On the positive side, the extra allocation is constant (or at least non-proportional with the usage) and it’s narrowing the window for other crashes during flushing (https://github.com/bitcoin/bitcoin/pull/30611 will also likely help here). This change may also enable another one (that I’m currently re-measuring to be sure), which seems to halve the remaining flush time again (by sorting the values in descending order before adding them to the batch), e.g. from 30 minutes (on master) to 10 (with this change included).

  10. luke-jr commented at 9:55 pm on January 14, 2025: member
    Can we predict the memory usage spike size? Presumably as we flush, that releases memory, which allows for a larger and larger batch size?
  11. l0rinc commented at 10:32 am on January 16, 2025: contributor

    Since profilers may not catch these short-lived spikes, I’ve instrumented the code, loaded the UTXO set (as described the in the PR), parsed the logged flushing times and memory usages and plotted them against each other to see the effect of the batch size increase.

      0diff --git a/src/txdb.cpp b/src/txdb.cpp
      1--- a/src/txdb.cpp	(revision d249a353be58868d41d2a7c57357038ffd779eba)
      2+++ b/src/txdb.cpp	(revision bae884969d35469320ed9967736eb15b5d87edff)
      3@@ -90,7 +90,81 @@
      4     return vhashHeadBlocks;
      5 }
      6
      7+/*
      8+ * Author:  David Robert Nadeau
      9+ * Site:    http://NadeauSoftware.com/
     10+ * License: Creative Commons Attribution 3.0 Unported License
     11+ *          http://creativecommons.org/licenses/by/3.0/deed.en_US
     12+ */
     13+#if defined(_WIN32)
     14+#include <windows.h>
     15+#include <psapi.h>
     16+
     17+#elif defined(__unix__) || defined(__unix) || defined(unix) || (defined(__APPLE__) && defined(__MACH__))
     18+#include <unistd.h>
     19+#include <sys/resource.h>
     20+
     21+#if defined(__APPLE__) && defined(__MACH__)
     22+#include <mach/mach.h>
     23+
     24+#elif (defined(_AIX) || defined(__TOS__AIX__)) || (defined(__sun__) || defined(__sun) || defined(sun) && (defined(__SVR4) || defined(__svr4__)))
     25+#include <fcntl.h>
     26+#include <procfs.h>
     27+
     28+#elif defined(__linux__) || defined(__linux) || defined(linux) || defined(__gnu_linux__)
     29+#include <stdio.h>
     30+
     31+#endif
     32+
     33+#else
     34+#error "Cannot define  getCurrentRSS( ) for an unknown OS."
     35+#endif
     36+
     37+/**
     38+ * Returns the current resident set size (physical memory use) measured
     39+ * in bytes, or zero if the value cannot be determined on this OS.
     40+ */
     41+size_t getCurrentRSS( )
     42+{
     43+#if defined(_WIN32)
     44+    /* Windows -------------------------------------------------- */
     45+    PROCESS_MEMORY_COUNTERS info;
     46+    GetProcessMemoryInfo( GetCurrentProcess( ), &info, sizeof(info) );
     47+    return (size_t)info.WorkingSetSize;
     48+
     49+#elif defined(__APPLE__) && defined(__MACH__)
     50+    /* OSX ------------------------------------------------------ */
     51+    struct mach_task_basic_info info;
     52+    mach_msg_type_number_t infoCount = MACH_TASK_BASIC_INFO_COUNT;
     53+    if ( task_info( mach_task_self( ), MACH_TASK_BASIC_INFO,
     54+        (task_info_t)&info, &infoCount ) != KERN_SUCCESS )
     55+        return (size_t)0L;      /* Can't access? */
     56+    return (size_t)info.resident_size;
     57+
     58+#elif defined(__linux__) || defined(__linux) || defined(linux) || defined(__gnu_linux__)
     59+    /* Linux ---------------------------------------------------- */
     60+    long rss = 0L;
     61+    FILE* fp = NULL;
     62+    if ( (fp = fopen( "/proc/self/statm", "r" )) == NULL )
     63+        return (size_t)0L;      /* Can't open? */
     64+    if ( fscanf( fp, "%*s%ld", &rss ) != 1 )
     65+    {
     66+        fclose( fp );
     67+        return (size_t)0L;      /* Can't read? */
     68+    }
     69+    fclose( fp );
     70+    return (size_t)rss * (size_t)sysconf( _SC_PAGESIZE);
     71+
     72+#else
     73+    /* AIX, BSD, Solaris, and Unknown OS ------------------------ */
     74+    return (size_t)0L;          /* Unsupported. */
     75+#endif
     76+}
     77+
     78 bool CCoinsViewDB::BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &hashBlock) {
     79+    const auto start = std::chrono::steady_clock::now();
     80+    size_t max_mem{getCurrentRSS()};
     81+
     82     CDBBatch batch(*m_db);
     83     size_t count = 0;
     84     size_t changed = 0;
     85@@ -129,7 +203,11 @@
     86         it = cursor.NextAndMaybeErase(*it);
     87         if (batch.SizeEstimate() > m_options.batch_write_bytes) {
     88             LogDebug(BCLog::COINDB, "Writing partial batch of %.2f MiB\n", batch.SizeEstimate() * (1.0 / 1048576.0));
     89+
     90+            max_mem = std::max(max_mem, getCurrentRSS());
     91             m_db->WriteBatch(batch);
     92+            max_mem = std::max(max_mem, getCurrentRSS());
     93+
     94             batch.Clear();
     95             if (m_options.simulate_crash_ratio) {
     96                 static FastRandomContext rng;
     97@@ -146,8 +224,16 @@
     98     batch.Write(DB_BEST_BLOCK, hashBlock);
     99
    100     LogDebug(BCLog::COINDB, "Writing final batch of %.2f MiB\n", batch.SizeEstimate() * (1.0 / 1048576.0));
    101+
    102+    max_mem = std::max(max_mem, getCurrentRSS());
    103     bool ret = m_db->WriteBatch(batch);
    104+    max_mem = std::max(max_mem, getCurrentRSS());
    105+
    106     LogDebug(BCLog::COINDB, "Committed %u changed transaction outputs (out of %u) to coin database...\n", (unsigned int)changed, (unsigned int)count);
    107+    if (changed > 0) {
    108+        const auto end{std::chrono::steady_clock::now()};
    109+        LogInfo("BatchWrite took=%dms, maxMem=%dMiB", duration_cast<std::chrono::milliseconds>(end - start).count(), max_mem >> 20);
    110+    }
    111     return ret;
    112 }
    
      0import os
      1import re
      2import shutil
      3import statistics
      4import subprocess
      5import time
      6import datetime
      7import argparse
      8import matplotlib.pyplot as plt  # python3.12 -m pip install matplotlib --break-system-packages
      9
     10# Regex to parse logs
     11BATCHWRITE_REGEX = re.compile(r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) BatchWrite took=(\d+)ms, maxMem=(\d+)MiB")
     12
     13
     14def parse_log(archive):
     15    """Parse the log file to extract elapsed times, flush times, and memory usage."""
     16    start_time = None
     17    elapsed, batchwrite_times, usage_snapshots = [], [], []
     18    with open(archive, "r") as f:
     19        for line in f:
     20            if m := BATCHWRITE_REGEX.search(line):
     21                dt = datetime.datetime.strptime(m.group(1), "%Y-%m-%dT%H:%M:%SZ")
     22                if start_time is None:
     23                    start_time = dt
     24                elapsed.append((dt - start_time).total_seconds())
     25                batchwrite_times.append(int(m.group(2)))
     26                usage_snapshots.append(int(m.group(3)))
     27    return elapsed, batchwrite_times, usage_snapshots
     28
     29
     30def plot_results(results, output_dir):
     31    """Create separate plots for flush times and memory usage."""
     32    if len(results) != 2:
     33        print("plot_results() requires exactly 2 runs for comparison.")
     34        return
     35
     36    (dbbatch0, elapsed0, flush0, mem0) = results[0]
     37    (dbbatch1, elapsed1, flush1, mem1) = results[1]
     38
     39    # Compute percentage differences
     40    avg_flush0, avg_flush1 = statistics.mean(flush0), statistics.mean(flush1)
     41    max_mem0, max_mem1 = max(mem0), max(mem1)
     42    flush_improvement = round(((avg_flush0 - avg_flush1) / avg_flush0) * 100, 1)
     43    mem_increase = round(((max_mem1 - max_mem0) / max_mem0) * 100, 1)
     44
     45    # Plot flush times
     46    plt.figure(figsize=(16, 8))
     47    plt.plot(elapsed0, flush0, color="red", linestyle="-", label=f"Flush Times (dbbatch={dbbatch0})")
     48    plt.axhline(y=avg_flush0, color="red", linestyle="--", alpha=0.5, label=f"Mean ({dbbatch0})={avg_flush0:.1f}ms")
     49    plt.plot(elapsed1, flush1, color="orange", linestyle="-", label=f"Flush Times (dbbatch={dbbatch1})")
     50    plt.axhline(y=avg_flush1, color="orange", linestyle="--", alpha=0.5, label=f"Mean ({dbbatch1})={avg_flush1:.1f}ms")
     51    plt.title(f"Flush Times (dbbatch {dbbatch0} vs {dbbatch1}) — {abs(flush_improvement)}% {'faster' if flush_improvement > 0 else 'slower'}")
     52    plt.xlabel("Elapsed Time (seconds)")
     53    plt.ylabel("Flush Times (ms)")
     54    plt.legend()
     55    plt.grid(True)
     56    plt.tight_layout()
     57    flush_out_file = os.path.join(output_dir, "plot_flush_times.png")
     58    plt.savefig(flush_out_file)
     59    print(f"Flush Times plot saved as {flush_out_file}")
     60    plt.close()
     61
     62    # Plot memory usage
     63    plt.figure(figsize=(16, 8))
     64    plt.plot(elapsed0, mem0, color="blue", linestyle="-", label=f"Memory (dbbatch={dbbatch0})")
     65    plt.axhline(y=max_mem0, color="blue", linestyle="--", alpha=0.5, label=f"Max Mem ({dbbatch0})={max_mem0}MiB")
     66    plt.plot(elapsed1, mem1, color="green", linestyle="-", label=f"Memory (dbbatch={dbbatch1})")
     67    plt.axhline(y=max_mem1, color="green", linestyle="--", alpha=0.5, label=f"Max Mem ({dbbatch1})={max_mem1}MiB")
     68    plt.title(f"Memory Usage (dbbatch {dbbatch0} vs {dbbatch1}) — {abs(mem_increase)}% {'higher' if mem_increase > 0 else 'lower'}")
     69    plt.xlabel("Elapsed Time (seconds)")
     70    plt.ylabel("Memory Usage (MiB)")
     71    plt.legend()
     72    plt.grid(True)
     73    plt.tight_layout()
     74    mem_out_file = os.path.join(output_dir, "plot_memory_usage.png")
     75    plt.savefig(mem_out_file)
     76    print(f"Memory Usage plot saved as {mem_out_file}")
     77    plt.close()
     78
     79
     80def loadtxoutset(dbbatchsize, datadir, bitcoin_cli, bitcoind, utxo_file):
     81    """Load the UTXO set and run the Bitcoin node."""
     82    archive = os.path.join(datadir, f"results_dbbatch-{dbbatchsize}.log")
     83
     84    # Skip if logs already exist
     85    if os.path.exists(archive):
     86        print(f"Log file {archive} already exists. Skipping loadtxoutset for dbbatchsize={dbbatchsize}.")
     87        return
     88
     89    os.makedirs(datadir, exist_ok=True)
     90    debug_log = os.path.join(datadir, "debug.log")
     91
     92    try:
     93        print("Cleaning up previous run")
     94        for subdir in ["chainstate", "chainstate_snapshot"]:
     95            shutil.rmtree(os.path.join(datadir, subdir), ignore_errors=True)
     96
     97        print("Preparing UTXO load")
     98        subprocess.run([bitcoind, f"-datadir={datadir}", "-stopatheight=1"], cwd=bitcoin_core_path)
     99        os.remove(debug_log)
    100
    101        print(f"Starting bitcoind with dbbatchsize={dbbatchsize}")
    102        subprocess.run([bitcoind, f"-datadir={datadir}", "-daemon", "-blocksonly=1", "-connect=0", f"-dbbatchsize={dbbatchsize}", f"-dbcache={440}"], cwd=bitcoin_core_path)
    103        time.sleep(5)
    104
    105        print("Loading UTXO set")
    106        subprocess.run([bitcoin_cli, f"-datadir={datadir}", "loadtxoutset", utxo_file], cwd=bitcoin_core_path)
    107    except Exception as e:
    108        print(f"Error during loadtxoutset for dbbatchsize={dbbatchsize}: {e}")
    109        raise
    110    finally:
    111        print("Stopping bitcoind...")
    112        subprocess.run([bitcoin_cli, f"-datadir={datadir}", "stop"], cwd=bitcoin_core_path, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    113        time.sleep(5)
    114
    115    shutil.copy2(debug_log, archive)
    116    print(f"Archived logs to {archive}")
    117
    118
    119if __name__ == "__main__":
    120    # Parse script arguments
    121    parser = argparse.ArgumentParser(description="Benchmark Bitcoin dbbatchsize configurations.")
    122    parser.add_argument("--utxo-file", required=True, help="Path to the UTXO snapshot file.")
    123    parser.add_argument("--bitcoin-core-path", required=True, help="Path to the Bitcoin Core project directory.")
    124    args = parser.parse_args()
    125
    126    utxo_file = args.utxo_file
    127    bitcoin_core_path = args.bitcoin_core_path
    128    datadir = os.path.join(bitcoin_core_path, "demo")
    129    debug_log = os.path.join(datadir, "debug.log")
    130    bitcoin_cli = os.path.join(bitcoin_core_path, "build/src/bitcoin-cli")
    131    bitcoind = os.path.join(bitcoin_core_path, "build/src/bitcoind")
    132
    133    # Build Bitcoin Core
    134    print("Building Bitcoin Core...")
    135    subprocess.run(["cmake", "-B", "build", "-DCMAKE_BUILD_TYPE=Release"], cwd=bitcoin_core_path, check=True)
    136    subprocess.run(["cmake", "--build", "build", "-j", str(os.cpu_count())], cwd=bitcoin_core_path, check=True)
    137
    138    # Run tests for each dbbatchsize
    139    results = []
    140    for dbbatchsize in [16777216, 67108864]:  # Original and proposed
    141        loadtxoutset(dbbatchsize, datadir, bitcoin_cli, bitcoind, utxo_file)
    142        archive = os.path.join(datadir, f"results_dbbatch-{dbbatchsize}.log")
    143        elapsed, batchwrite_times, usage_snapshots = parse_log(archive)
    144        results.append((dbbatchsize, elapsed, batchwrite_times, usage_snapshots))
    145
    146    # Plot results
    147    plot_results(results, bitcoin_core_path)
    148    print("All configurations processed.")
    

    For standard dbcache values the results are very close (though the memory measurements aren’t as scientific as I’d like them to be (probably because there is still enough memory), some runs even indicate that 16MiB consumes a bit more memory than the 64MiB version), but the trend seems to be clear from the produced plots: the batch writes are faster (and seem more predictable) with bigger batches, while the memory usage is only slightly higher.

    plot_flush_times plot_memory_usage

    Is there any other way that you’d like me to test this @sipa, @luke-jr, @1440000bytes?

  12. DrahtBot added the label Needs rebase on Jan 16, 2025
  13. l0rinc force-pushed on Jan 16, 2025
  14. luke-jr commented at 10:56 pm on January 16, 2025: member

    I think those graphs need to be on height rather than seconds. The larger dbbatchsize making it faster means it gets further in the chain, leading to the higher max at the end…

    I would expect both lines to be essentially overlapping except during flushes.

  15. l0rinc commented at 10:14 am on January 17, 2025: contributor

    I would expect both lines to be essentially overlapping except during flushes.

    I was only measuring the memory here during flushes. There is no direct height available there, but if we instrument UpdateTipLog instead (and fetch some data from the assumeUTXO height), we’d get:

    dbbatchsize=16MiB:

    image

    dbbatchsize=64MiB (+ experimental sorting):

    image


    overlapped (blue 16, green 64): image

  16. DrahtBot removed the label Needs rebase on Jan 17, 2025
  17. luke-jr referenced this in commit aa56eaaa78 on Feb 22, 2025
  18. l0rinc renamed this:
    optimization: increase default LevelDB write batch size to 64 MiB
    [IBD] Flush UXTOs in bigger batches
    on Mar 12, 2025
  19. l0rinc renamed this:
    [IBD] Flush UXTOs in bigger batches
    [IBD] flush UXTOs in bigger batches
    on Mar 12, 2025
  20. ryanofsky approved
  21. ryanofsky commented at 7:32 pm on March 12, 2025: contributor

    Code review ACK 868413340f8d6058d74186b65ac3498d6b7f254a

    If that spike exceeds the memory the process can allocate it causes a crash, at a particularly bad time (may require a replay to fix, which may be slower than just reprocessing the blocks).

    It is difficult for me to have a sense of how safe this change is but I’d hope we are not currently pushing systems so close to the edge that using an extra 48mb will cause them to start crashing. This does seem like a nice performance improvment if it doesn’t cause crashes.

    In theory, we could dynamically limit the batch size based on available memory to mitigate the risk of crashes. However, since the batch size is already small and further increases don’t provide much additional benefit (per the commit message), that added complexity probably isn’t worth it here.

  22. jonatack commented at 4:08 pm on March 13, 2025: member

    Concept ACK on raising the default from 16 to 67 megabytes (note that -dbbatchsize is a hidden -help-debug config option). Testing.

    0$ ./build/bin/bitcoind -help-debug | grep -A2 dbbatch
    1  -dbbatchsize
    2       Maximum database write batch size in bytes (default: 67108864)
    
  23. jonatack commented at 8:22 pm on March 13, 2025: member

    Review and testing-under-way ACK 868413340f8d6058d74186b65ac3498d6b7f254a

    As described in the pull description, this value was set in 2017 in #10148 and hasn’t been changed since.

    Maybe a config option can be provided to change it.

    Just realized dbbatchsize already exists.

    This is a hidden config option and the rationale might be #10148 (review):

    “the relevant constraint is the memory usage peak from allocating the batch, which depends on the batch memory usage, not dbcache memory usage. Also, I don’t think anyone will need to change this property (except for tests, where it’s very useful to get much more frequent partial flushes).”

    Though, if an older machine might be constrained in memory during IBD after upgrading bitcoin core, that might be an argument to unhide -dbbatchsize with the larger default (and document its use in the conf file; I don’t recall if conf file doc gen is automated yet).

  24. l0rinc commented at 1:24 pm on March 14, 2025: contributor

    Reran the benchmarks on a Hetzner Linux machine using GCC with the new AssumeUTXO 880k set, parsing the logged flush times and plotting the results. For different dbcache (440, 5000, 30000) and dbbatchsize (4-256MiB range, 16MiB (current) and 64MiB (proposed) are highlighted and trendline is added for clarity) sizes, each one twice for stability:

    Sorting the same measurements (+ sums) might give us a better understanding of the trends:


    Edit: running it on my Mac M4 Max gives more varied results in general:


    I will investigate if it depends on the compiler and operating system - can anyone else check Windows?

  25. Eunovo commented at 3:01 pm on March 20, 2025: contributor

    I used valgrind --tool=massif valgrind massif to profile memory usage of bitcoin-core IBD(840002 to 850000) for 16MiB and 64MiB batch sizes. Tested on an Ubuntu 22.04 Server and compiled with GCC 11.4.0.

    Memory Usage Tests

    Method

     0mkdir demo
     1build/bin/bitcoind -datadir=demo -stopatheight=1
     2build/bin/bitcoind -datadir=demo -daemon -blocksonly=1 -stopatheight=840001
     3build/bin/bitcoin-cli -datadir=demo -rpcclienttimeout=0 loadtxoutset ~/utxo-840000.dat
     4mkdir demo-backup
     5cp -r demo/* demo-backup
     6
     7valgrind --tool=massif build/bin/bitcoind -datadir=demo -daemon -blocksonly=1 -dbbatchsize=16777216 -stopatheight=850000
     8
     9rm -rf demo
    10mkdir demo
    11cp -r demo-backup/* demo
    12
    13valgrind --tool=massif build/bin/bitcoind -datadir=demo -daemon -blocksonly=1 -dbbatchsize=67108864 -stopatheight=850000
    

    Results

    Running ms_print on the massif output files produces

     0--------------------------------------------------------------------------------
     1Command:            build/bin/bitcoind -datadir=../demo -daemon -blocksonly=1 -dbbatchsize=16777216 -stopatheight=850000
     2Massif arguments:   (none)
     3ms_print arguments: massif.out.674977
     4--------------------------------------------------------------------------------
     5
     6
     7    MB
     8811.4^        #                                                               
     9     |     :: #                                                       :       
    10     |     :  #                                                       :       
    11     |     :  #             ::               ::                       :       
    12     |  :  :  # @           :                :           :   :        :       
    13     |  :  :  # @          ::  :   ::        :   @@      :   :   :    :  ::  @
    14     |  :  : :# @   :  ::  ::  :   :        ::   @   :   :   :   :    :  :   @
    15     | ::  : :# @ :::  :   ::  :   :  ::    ::   @   :   :  ::   :  :::  :   @
    16     | ::  : :# @ : :  :  :::  :   :  :   :::: ::@   :  ::  ::   :  : :  :   @
    17     | ::  : :#:@:: :  :  :::  : :::  :   : :: : @   :  ::  :: :::  : :  :   @
    18     | ::::: :#:@:: :  :  ::: @: : :  :   : :: : @   :  ::  :: : :  : ::::   @
    19     | ::: : :#:@:: :::: :::: @: : : ::   : :: : @   :  :::::: : :::: :: : ::@
    20     | ::: : :#:@:: :: : :::: @: : : :: ::: :: : @   :@@::: :::: :: : :: : : @
    21     | ::: : :#:@:: :: : :::: @::: : :: : : :: : @ :::@ ::: :::: :: : :: : : @
    22     | ::: : :#:@:: :: : :::: @::: : :: : : :: : @ : :@ ::: :::: :: : :: : : @
    23     | ::: : :#:@:: :: : :::: @::: : :: : : :: : @ : :@ ::: :::: :: : :: : : @
    24     | ::: : :#:@:: :: : :::: @::: : :: : : :: : @ : :@ ::: :::: :: : :: : : @
    25     | ::: : :#:@:: :: : :::: @::: : :: : : :: : @ : :@ ::: :::: :: : :: : : @
    26     | ::: : :#:@:: :: : :::: @::: : :: : : :: : @ : :@ ::: :::: :: : :: : : @
    27     | ::: : :#:@:: :: : :::: @::: : :: : : :: : @ : :@ ::: :::: :: : :: : : @
    28   0 +----------------------------------------------------------------------->Ti
    29     0                                                                   8.610
    
     0--------------------------------------------------------------------------------
     1Command:            build/bin/bitcoind -datadir=../demo -daemon -blocksonly=1 -dbbatchsize=67108864 -stopatheight=850000
     2Massif arguments:   (none)
     3ms_print arguments: massif.out.682470
     4--------------------------------------------------------------------------------
     5
     6
     7    GB
     81.005^          ##                                                            
     9     |          #                        ::                                   
    10     |          #               :        :   ::                               
    11     |          #               :        :   :                        :       
    12     |          #               :        :   :                        :       
    13     |          #               :        :   :                        :      :
    14     |          #               :        :   :        :               :      :
    15     |  :  ::   #      :    :   :        :  ::   :    : ::   :   ::   :   :: :
    16     |  :  : @  #      :    :  ::  :   :::  ::   :    : :    :   :  :::   :  @
    17     |  :  : @  # ::   :    : :::  :   : : :::  ::  ::: :  :::  ::  : : :::  @
    18     |  :  : @  # :   ::  ::: :::  :   : : :::  ::  : : :  : :  ::  : : : : :@
    19     | ::::: @::# :   ::  : : :::  :  :: : ::: :::  : :::  : :  :: :: ::: : :@
    20     | ::: : @: # :   ::  : : ::::::  :: : ::: :::  : :::  : :  :: :: ::: : :@
    21     | ::: : @: # : ::::  : :::::: :  :: : ::: :::::: :::  : ::::: :: ::: : :@
    22     | ::: : @: # : : ::::: :::::: ::::: : ::: :::: : ::: :: :: :: :: ::: : :@
    23     | ::: : @: # : : ::: : :::::: :: :: : ::: :::: : ::: :: :: :: :: ::: : :@
    24     | ::: : @: # : : ::: : :::::: :: :: : ::: :::: : ::: :: :: :: :: ::: : :@
    25     | ::: : @: # : : ::: : :::::: :: :: : ::: :::: : ::: :: :: :: :: ::: : :@
    26     | ::: : @: # : : ::: : :::::: :: :: : ::: :::: : ::: :: :: :: :: ::: : :@
    27     | ::: : @: # : : ::: : :::::: :: :: : ::: :::: : ::: :: :: :: :: ::: : :@
    28   0 +----------------------------------------------------------------------->Ti
    29     0                                                                   8.477
    

    The graphs show that with the default dbcache size, bitcoin-core memory usage peaks at 811.4MB for 16MiB dbbatchsize and 1.005GB for 64MiB dbbatchsize .

    If I’m not mistaken, Bitcoin-core runs on really small devices like Raspberry Pi with only 1GB RAM, if we raise the default dbbatchsize to 64GB, there’s a chance that Bitcoin-core could OOM if there’s not enough Swap space. Also note that if the bitcoin-core process uses swap space, it will run slower thereby negating potential speed gains from raising the dbbatchsize.

    Speed Tests

    Method

    Apply the following diff to output Batch write time data to debug.log.

     0diff --git a/src/txdb.cpp b/src/txdb.cpp
     1index 1622039d63..ef73378ce6 100644
     2--- a/src/txdb.cpp
     3+++ b/src/txdb.cpp
     4@@ -91,6 +91,7 @@ std::vector<uint256> CCoinsViewDB::GetHeadBlocks() const {
     5 }
     6 
     7 bool CCoinsViewDB::BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &hashBlock) {
     8+    const auto start = std::chrono::steady_clock::now();
     9     CDBBatch batch(*m_db);
    10     size_t count = 0;
    11     size_t changed = 0;
    12@@ -147,7 +148,9 @@ bool CCoinsViewDB::BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &hashB
    13 
    14     LogDebug(BCLog::COINDB, "Writing final batch of %.2f MiB\n", batch.SizeEstimate() * (1.0 / 1048576.0));
    15     bool ret = m_db->WriteBatch(batch);
    16+    const auto end{std::chrono::steady_clock::now()};
    17     LogDebug(BCLog::COINDB, "Committed %u changed transaction outputs (out of %u) to coin database...\n", (unsigned int)changed, (unsigned int)count);
    18+    LogDebug(BCLog::COINDB, "BatchWrite: completed in %dms\n", duration_cast<std::chrono::milliseconds>(end - start).count());
    19     return ret;
    20 }
    
     0mkdir demo
     1build/bin/bitcoind -datadir=demo -stopatheight=1
     2build/bin/bitcoind -datadir=demo -daemon -blocksonly=1 -stopatheight=840001
     3build/bin/bitcoin-cli -datadir=demo -rpcclienttimeout=0 loadtxoutset ~/utxo-840000.dat
     4mkdir demo-backup
     5cp -r demo/* demo-backup
     6
     7build/bin/bitcoind -datadir=demo -daemon -blocksonly=1 -dbbatchsize=16777216 -stopatheight=850000 -debug=coindb
     8cp demo/debug.log defaultcache_16MiB_dbbatchsize.log
     9
    10rm -rf demo
    11mkdir demo
    12cp -r demo-backup/* demo
    13
    14build/bin/bitcoind -datadir=demo -daemon -blocksonly=1 -dbbatchsize=67108864 -stopatheight=850000 -debug=coindb
    15cp demo/debug.log defaultcache_64MiB_dbbatchsize.log
    16
    17// Parse log files using https://github.com/Eunovo/31645-data/blob/main/extract_batch_write_data.py
    18cat defaultcache_64MiB_dbbatchsize.log | grep "BatchWrite" -B 1 | xargs ./extract_batch_write_data.py > defaultcache_64MiB_dbbatchsize.result
    

    Results

     0Logfile: defaultcache_16MiB_dbbatchsize.log
     1--------------------------------------------
     2Average num_txouts_per_ms: 1018.2494977251025
     3--------------------------------------------
     4num_txouts,write_times_ms,num_txouts_per_ms
     52862776,2830,1011.5816254416961
     62934562,2851,1029.3097158891617
     72921143,2753,1061.0762804213584
     82940809,2861,1027.8954910870325
     92948759,2870,1027.4421602787456
    102979042,2967,1004.0586450960566
    112956426,2917,1013.5159410353102
    123048466,3060,996.2307189542483
    132978184,2940,1012.9877551020409
    142944033,2945,999.6716468590832
    152973528,2970,1001.1878787878788
    162970816,2889,1028.3198338525442
    172982352,2898,1029.1069703243616
    182960008,2776,1066.28530259366
    193016540,2952,1021.8631436314363
    203007010,3033,991.4309264754369
    212976775,2982,998.2478202548625
    223084773,3085,999.92641815235
    232069630,2016,1026.6021825396826
    
     0Logfile: defaultcache_64MiB_dbbatchsize.log
     1--------------------------------------------
     2Average num_txouts_per_ms: 735.7374156918239
     3--------------------------------------------
     4num_txouts,write_times_ms,num_txouts_per_ms
     52862776,3881,737.6387528987375
     62895729,3942,734.5837138508372
     72941575,3704,794.161717062635
     82941158,3924,749.5305810397554
     92965901,4129,718.3097602325018
    102978043,4095,727.2388278388279
    112946445,4041,729.1375897055184
    123034443,4319,702.5799953692984
    132993584,4154,720.6509388541165
    142947744,4113,716.6895210308777
    152979400,4072,731.679764243615
    162968346,3819,777.2573972244043
    172991885,3932,760.9066632756867
    182960911,3844,770.2682101977107
    192942739,4000,735.68475
    203031336,4282,707.9252685660906
    213007839,4118,730.4125789218067
    223057094,4257,718.133427296218
    232137921,2985,716.2214405360133
    

    Running with 64MiB dbbatchsize turns out to be about 27% slower. I was only able to oberserve a speedup when doing a large flush which doesn’t happen often with the default 450MiB cache size.

  26. sipa commented at 3:41 pm on March 20, 2025: member
    Maybe it’s a better approach to scale the (default) batch size in function of the dbcache itself? That way, people who have configured their node to run with a low memory footprint to begin with will have relatively small flushing spikes, but people who run on beefy systems and thus likely can tolerate bigger spikes too get to enjoy the performance benefit those may be provide?
  27. l0rinc renamed this:
    [IBD] flush UXTOs in bigger batches
    [IBD] flush UTXOs in bigger batches
    on Mar 25, 2025
  28. l0rinc commented at 3:23 pm on March 26, 2025: contributor

    Thanks for your measurements @Eunovo!

    I’m not sure if we support 1GiB of total memory or not, but even 8 years ago that required a lower dbcache that 450MiB.

    It took me a bit, but I finally finished comparing the 16 and 64 MiB batches with valgrind --tool=massif for full IBD until block 888888.

     0COMMITS="998386d4462f5e06412303ba559791da83b913fb 5cefc4bbadaa06f22089dd7991fac17ed5b283ca"; \
     1STOP_HEIGHT=888888; DBCACHE=1000; \
     2C_COMPILER=gcc; CXX_COMPILER=g++; \
     3DATA_DIR="/mnt/my_storage/BitcoinData"; \
     4LOG_DIR="/mnt/my_storage/logs"; \
     5mkdir -p $LOG_DIR; \
     6for COMMIT in $COMMITS; do \
     7  git fetch --all -q && git fetch origin $COMMIT && git log -1 --oneline $COMMIT && \
     8  killall bitcoind || true && \
     9  rm -rf $DATA_DIR/* && \
    10  git checkout $COMMIT && \
    11  git clean -fxd && \
    12  git reset --hard && \
    13  cmake -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_WALLET=OFF -DCMAKE_C_COMPILER=$C_COMPILER -DCMAKE_CXX_COMPILER=$CXX_COMPILER && \
    14  cmake --build build -j$(nproc) --target bitcoind && \
    15  ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=1 -printtoconsole=1 || true && \
    16  valgrind --tool=massif --time-unit=ms \
    17    --massif-out-file="$LOG_DIR/massif-$COMMIT-$STOP_HEIGHT-$DBCACHE.out" \
    18    ./build/bin/bitcoind -datadir=$DATA_DIR \
    19    -stopatheight=$STOP_HEIGHT \
    20    -dbcache=$DBCACHE \
    21    -blocksonly=1 \
    22    -printtoconsole=1 && \
    23  cp $DATA_DIR/debug.log "$LOG_DIR/debug-$COMMIT-$STOP_HEIGHT-$DBCACHE.log" && \
    24  ms_print "$LOG_DIR/massif-$COMMIT-$STOP_HEIGHT-$DBCACHE.out" | tee "$LOG_DIR/massif-$COMMIT-$STOP_HEIGHT-$DBCACHE.txt"; \
    25done
    

    The memory difference is measurable and non-negligible (~200MiB, seems the buffer may be copied 4 times), especially for small memory usecases. I have opened https://github.com/google/leveldb/pull/1259 to enable pre-sizing the memory, which might help us in reducing the spikes slightly.

    16 » 10 with 1GiB dbcache:

    64 » 10 with 1GiB dbcache:


    I like @sipa’s suggestion of making the batch size depend on cache size, I also thought of that while investigating it, but that may mean that the hidden dbbatchsize has to be changed. Basically we could have a fixed number of batch fills, currently 28 GiB is batched into 16 MiB chunks, we could set it to 1000 fixed writes - or something similar, I’ll investigate.


    And about the speedups, I found that on Mac the differences were tiny (see attachments), but on Linux they were basically always faster, regardless of the dbcache size. We have to find out in which case it’s faster and when it’s slower, thanks for helping me with that.

  29. Eunovo commented at 6:05 am on March 27, 2025: contributor

    The memory difference is measurable and non-negligible (~200MiB, seems the buffer may be copied 4 times), especially for small memory usecases. I have opened google/leveldb#1259 to enable pre-sizing the memory, which might help us in reducing the spikes slightly. @l0rinc you can also check leveldb.approximate-memory-usage. Use db.GetProperty before and after batch writes. I noticed that it shows a non-negligible spike in memory usage. Could be useful to quickly check if https://github.com/google/leveldb/pull/1259 works

  30. l0rinc marked this as a draft on Mar 30, 2025
  31. l0rinc commented at 3:47 pm on March 30, 2025: contributor

    I’ve experimented a bit with changing the fixed write batch size to a dynamic value that scales with dbcache size, basically something like:

    0static int64_t GetDefaultDbBatchSize(int64_t dbcache_bytes) {
    1    return std::min<int64_t>(
    2        MAX_DB_CACHE_BATCH, // 512 MiB upper limit
    3        std::max<int64_t>(
    4            MIN_DB_CACHE,   // 4 MiB lower limit
    5            dbcache_bytes * DEFAULT_KERNEL_CACHE_BATCH_SIZE / DEFAULT_KERNEL_CACHE
    6        )
    7    );
    8}
    

    Corresponding to:

    dbcache (MiB) Before (Fixed) After (Dynamic)
    Batch Size Batch Count Batch Size Batch Count
    4 16 MiB 1,546 4 MiB 3,111
    10 16 MiB 1,546 4 MiB 3,090
    45 16 MiB 780 4 MiB 3,090
    100 16 MiB 929 4 MiB 3,085
    450 (default) 16 MiB 775 19.6 MiB 646
    1,000 16 MiB 788 44 MiB 285
    4,500 16 MiB 774 200 MiB 69
    10,000 16 MiB 772 444 MiB 33
    45,000 16 MiB 771 512 MiB 28

    This would:

    • Provide smaller batches (4 MiB) for memory-constrained systems (likely making them a bit slower).
    • Maintains a similar batch size for default dbcache (20 MiB instead of 16 MiB, making it a bit faster, using a bit more memory).
    • Scales to larger batches for high-memory systems (up to 512 MiB, making it a lot faster, using a lot more memory).

    The chart illustrates how the dynamic batch sizing maintains approximately a 1:22 ratio (~4.5%) between batch size and dbcache size, while capping at 4 MiB minimum for memory-constrained systems and 512 MiB maximum for high-memory systems (I chose this simply to maintain a similar batch size for the default value (20 MiB instead of 16 MiB for dbcache=440) while scaling linearly for other values).

    I have drafted the PR until I fine-tune these values based on performance and memory usage.

  32. l0rinc force-pushed on Apr 8, 2025
  33. l0rinc commented at 10:11 pm on April 8, 2025: contributor

    TLDR: new version has dynamic batch size, memory and performance stays the same for the default -dbcache=450 and below. The memory and performance increase gradually for higher dbcache values. Measured memory peaks don’t exceed 12% of dbcache, batch size is capped at 256MiB, AssumeUTXO speedup is ~10-20%, reindex-chainstate and IBD performance is measured collectively in #32043. PR description was rewritten from scratch, PR is ready for review again.


    Ended up with keeping the 16 MiB batch size for dbcache sizes below-or-equal to the default (this should simplify things):

    $$ \text{GetDbBatchSize}(dbcache) = \max\Bigl(16\mathrm{MiB},
    \min\bigl(256\mathrm{MiB},
    \frac{dbcache}{450\mathrm{MiB}} \times 16\mathrm{MiB} \bigr) \Bigr). $$

    Which produces:

    0           4 |          16.0 |     832
    1          10 |          16.0 |     832
    2          45 |          16.0 |     832
    3         100 |          16.0 |     832
    4         450 |          16.0 |     832
    5        1000 |          35.6 |     375
    6        2000 |          71.1 |     188
    7        4500 |         160.0 |      84
    8       10000 |         256.0 |      52
    9       45000 |         256.0 |      52
    

    Which corresponds to the following peak memory increase per dbcache:

    0         450 |     0.753 |    0.755 |       0.001 |       0.2% | 16 MiB → 16 MiB
    1        1000 |     1.240 |    1.313 |       0.073 |       5.9% | 16 MiB → 35 MiB
    2        4500 |     4.564 |    5.109 |       0.545 |      11.9% | 16 MiB → 128 MiB
    3       10000 |     9.785 |   10.770 |       0.985 |      10.1% | 16 MiB → 256 MiB
    

    256MiB was chosen as the top value because bigger batches didn’t show any further improvement:

    Speedup depends on the dbcache size now - more available memory allows bigger batches:

  34. l0rinc marked this as ready for review on Apr 8, 2025
  35. l0rinc renamed this:
    [IBD] flush UTXOs in bigger batches
    [IBD] flush UTXOs in bigger batches based on dbcache size
    on Apr 8, 2025
  36. l0rinc force-pushed on Apr 8, 2025
  37. DrahtBot added the label CI failed on Apr 8, 2025
  38. DrahtBot commented at 10:21 pm on April 8, 2025: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/40211589754

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  39. l0rinc force-pushed on Apr 8, 2025
  40. l0rinc force-pushed on Apr 8, 2025
  41. l0rinc force-pushed on Apr 8, 2025
  42. l0rinc force-pushed on Apr 8, 2025
  43. l0rinc force-pushed on Apr 9, 2025
  44. l0rinc force-pushed on Apr 9, 2025
  45. l0rinc force-pushed on Apr 9, 2025
  46. l0rinc force-pushed on Apr 9, 2025
  47. l0rinc force-pushed on Apr 9, 2025
  48. l0rinc commented at 9:00 am on April 9, 2025: contributor
    Seems ""_MiB is explicitly failing for large values - even if I’m just using it for test calculations which end up fitting into 32 bits anyway - so I ended up excluding those tests on 32 bit system since we can’t have dbcache that large there anyway: https://github.com/bitcoin/bitcoin/compare/cc8d5446a6ca5f5e0566f062ab2e15ae5b6186c4..31f8394f6d5c22aa8d38dc479a6c02e887b8e790#diff-3d0856e8b7f136c588b229e0cbd3b2e2c309cd097ade0879521daba4e1bb2a33R1089-R1094
  49. DrahtBot removed the label CI failed on Apr 9, 2025
  50. l0rinc requested review from ryanofsky on Apr 9, 2025
  51. l0rinc requested review from jonatack on Apr 9, 2025
  52. in src/node/coins_view_args.h:21 in b249239c17 outdated
    18+    {
    19+        const auto target{(dbcache_bytes / DEFAULT_KERNEL_CACHE) * DEFAULT_DB_CACHE_BATCH};
    20+        return std::max<size_t>(MIN_DB_CACHE_BATCH, std::min<size_t>(MAX_DB_CACHE_BATCH, target));
    21+    }
    22+
    23+    void ReadCoinsViewArgs(const ArgsManager& args, CoinsViewOptions& options);
    


    ryanofsky commented at 4:46 pm on April 14, 2025:

    In commit “coins: introduce dynamic batch size calculator based on dbcache value” (b249239c1747a5425e5161974d2b9c9c5be17dab)

    Would be better to restore previous indentation and follow the “4 space indentation (no tabs) for every block except namespaces” rule in developer notes. Namespaces reflect directory paths and project organization beyond the scope of an individual file and are not intended to effect indentation within files.


    l0rinc commented at 11:11 am on April 16, 2025:
    Sure, done
  53. in src/node/coins_view_args.h:18 in b249239c17 outdated
    15+namespace node
    16+{
    17+    static constexpr size_t GetDbBatchSize(const size_t dbcache_bytes)
    18+    {
    19+        const auto target{(dbcache_bytes / DEFAULT_KERNEL_CACHE) * DEFAULT_DB_CACHE_BATCH};
    20+        return std::max<size_t>(MIN_DB_CACHE_BATCH, std::min<size_t>(MAX_DB_CACHE_BATCH, target));
    


    ryanofsky commented at 4:51 pm on April 14, 2025:

    In commit “coins: introduce dynamic batch size calculator based on dbcache value” (b249239c1747a5425e5161974d2b9c9c5be17dab)

    Not important, but seems like this could be simplified with std::clamp

    0return std::clamp(target, MIN_DB_CACHE_BATCH, MAX_DB_CACHE_BATCH);
    

    l0rinc commented at 11:11 am on April 16, 2025:
    Indeed, a lot cleaner this way, I can even inline target now, thanks!

    l0rinc commented at 10:47 am on April 23, 2025:

    https://en.cppreference.com/w/cpp/algorithm/clamp states that

    If lo is greater than hi, the behavior is undefined.

    (cc: @hodlinator) Should we add an explicit assert somewhere that this isn’t the case? Though it’s implied currently from the tests…


    hodlinator commented at 1:23 pm on April 23, 2025:
    I think it might make sense to introduce our own version of clamp that includes such an assert since the standard library has no guarantees if cppreference is to be believed. (Or just start off with an assert/Assume before the call for now).

    l0rinc commented at 0:17 am on August 7, 2025:
    Ended up with extracting min/max and adding an assert
  54. in src/kernel/caches.h:20 in b249239c17 outdated
    15@@ -16,6 +16,10 @@ static constexpr size_t MAX_BLOCK_DB_CACHE{2_MiB};
    16 //! Max memory allocated to coin DB specific cache (bytes)
    17 static constexpr size_t MAX_COINS_DB_CACHE{8_MiB};
    18 
    19+static constexpr size_t MIN_DB_CACHE_BATCH{16_MiB};
    20+static constexpr size_t DEFAULT_DB_CACHE_BATCH{16_MiB}; // The batch size of DEFAULT_KERNEL_CACHE
    


    ryanofsky commented at 4:55 pm on April 14, 2025:

    In commit “coins: introduce dynamic batch size calculator based on dbcache value” (b249239c1747a5425e5161974d2b9c9c5be17dab)

    Maybe there should be a test or static assert somewhere that DEFAULT_DB_CACHE_BATCH == GetDbBatchSize(DEFAULT_KERNEL_CACHE)


    l0rinc commented at 11:10 am on April 16, 2025:

    Added it to the test as:

    0BOOST_REQUIRE_EQUAL(node::GetDbBatchSize(DEFAULT_KERNEL_CACHE), DEFAULT_DB_CACHE_BATCH);
    
  55. in src/kernel/caches.h:19 in 31f8394f6d outdated
    15@@ -16,6 +16,10 @@ static constexpr size_t MAX_BLOCK_DB_CACHE{2_MiB};
    16 //! Max memory allocated to coin DB specific cache (bytes)
    17 static constexpr size_t MAX_COINS_DB_CACHE{8_MiB};
    18 
    19+static constexpr size_t MIN_DB_CACHE_BATCH{16_MiB};
    


    ryanofsky commented at 5:09 pm on April 14, 2025:

    In commit “coins: introduce dynamic batch size calculator based on dbcache value” (b249239c1747a5425e5161974d2b9c9c5be17dab)

    Name suggests this is a minimum size, but value is actually ignored if -dbbatchsize is specified, so is not an enforced minimum. Would suggest calling it a “minimum default” value instead of a “minimum” value to be more accurate. Or it might be better to just drop MIN_DB_CACHE_BATCH and MAX_DB_CACHE_BATCH here and just inline these in the GetDbBatchSize function since they are not used anywhere outside it.


    l0rinc commented at 11:10 am on April 16, 2025:
    Inlined max and eliminated min (replaced with default)
  56. in src/node/coins_view_args.h:17 in 31f8394f6d outdated
    14-void ReadCoinsViewArgs(const ArgsManager& args, CoinsViewOptions& options);
    15+namespace node
    16+{
    17+    static constexpr size_t GetDbBatchSize(const size_t dbcache_bytes)
    18+    {
    19+        const auto target{(dbcache_bytes / DEFAULT_KERNEL_CACHE) * DEFAULT_DB_CACHE_BATCH};
    


    ryanofsky commented at 5:12 pm on April 14, 2025:

    In commit “coins: introduce dynamic batch size calculator based on dbcache value” (b249239c1747a5425e5161974d2b9c9c5be17dab)

    Because division is happening before multiplication here, target will be 0 for any value below DEFAULT_KERNEL_CACHE, instead of scaling smoothly. This works well enough for current code because MIN_DB_CACHE_BATCH and DEFAULT_DB_CACHE_BATCH are equal, but if the minimum was less, this would return a discontinuous result instead of scaling smoothly. Would suggest either adding a conditional to handle this case or just using the default as the minimum explicitly here.


    l0rinc commented at 11:12 am on April 16, 2025:

    Since division needs to be before multiplication to avoid overflow of size_t on 32 bits, the simplest solution I found was to eliminate the min and use default as the min value for calculation instead:

    0static constexpr size_t GetDbBatchSize(const size_t dbcache_bytes)
    1{
    2    return std::clamp(
    3        (dbcache_bytes / DEFAULT_KERNEL_CACHE) * DEFAULT_DB_CACHE_BATCH,
    4        /*lo=*/DEFAULT_DB_CACHE_BATCH,
    5        /*hi=*/256_MiB
    6    );
    7}
    

    Added test for the min value to correspond to the default:

    0BOOST_REQUIRE_EQUAL(node::GetDbBatchSize(0_MiB), DEFAULT_DB_CACHE_BATCH);
    

    Updated the commit message with:

    The minimum coincides with the default to prevent performance degradation for small caches and avoid discontinuities from integer division.

    What do you think?

  57. ryanofsky approved
  58. ryanofsky commented at 5:19 pm on April 14, 2025: contributor

    Code review ACK 31f8394f6d5c22aa8d38dc479a6c02e887b8e790. I don’t have much insight into the performance / stability tradeoff but implementation of this seems very reasonable and should avoid affecting memory constrainted systems.

    Left some suggestions about the implementation below but none are very important.

  59. DrahtBot requested review from jonatack on Apr 15, 2025
  60. in src/init.cpp:490 in 31f8394f6d outdated
    486@@ -487,7 +487,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
    487     argsman.AddArg("-coinstatsindex", strprintf("Maintain coinstats index used by the gettxoutsetinfo RPC (default: %u)", DEFAULT_COINSTATSINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
    488     argsman.AddArg("-conf=<file>", strprintf("Specify path to read-only configuration file. Relative paths will be prefixed by datadir location (only useable from command line, not configuration file) (default: %s)", BITCOIN_CONF_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
    489     argsman.AddArg("-datadir=<dir>", "Specify data directory", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
    490-    argsman.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: %u)", nDefaultDbBatchSize), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
    491+    argsman.AddArg("-dbbatchsize", "Maximum database write batch size in bytes (calculated dynamically by default)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
    


    jonatack commented at 9:38 pm on April 15, 2025:

    Possibly naive suggestion: could this help doc indicate what value will be used by default, based on the actual dbcache value (if there isn’t an initialization order issue, didn’t check). Currently this only returns the following, which makes it difficult to know what will change if a value is provided by the user:

    0$ ./build/bin/bitcoind -help-debug | grep -C2 dbbatchsize
    1  -dbbatchsize
    2       Maximum database write batch size in bytes (calculated dynamically by
    3       default)
    

    l0rinc commented at 9:39 pm on April 15, 2025:

    You mean we should add the formula to the doc?

    $$ \text{GetDbBatchSize}(dbcache) = \max\Bigl(16\mathrm{MiB},
    \min\bigl(256\mathrm{MiB},
    \frac{dbcache}{450\mathrm{MiB}} \times 16\mathrm{MiB} \bigr) \Bigr). $$


    jonatack commented at 9:44 pm on April 15, 2025:
    -dbcache is a config option set only on init, so my thinking was that maybe this help doc might use that value and give the actual amount.

    l0rinc commented at 9:50 pm on April 15, 2025:
    The user shouldn’t really set this, it’s a hidden, ArgsManager::DEBUG_ONLY arg. I’m not sure if it’s possible to set it dynamically, can you give me a patch if it is?

    jonatack commented at 9:52 pm on April 15, 2025:
    Yes, but the config option is presumably there for developers to use or test with (otherwise it can be removed), and ISTM that it shouldn’t require code review or customization to know what value will be used by default. I could be wrong, though.

    l0rinc commented at 11:10 am on April 16, 2025:

    You mean something like:

    0strprintf("Maximum database write batch size in bytes (default: %u)", node::GetDbBatchSize(argsman.GetIntArg("-dbcache", DEFAULT_DB_CACHE)))
    

    It seems dbcache isn’t available here yet (it always gets the default DEFAULT_KERNEL_CACHE):

    0./build/bin/bitcoind -dbcache=1000 -help-debug | grep -A1 dbbatchsize
    1  -dbbatchsize
    2       Maximum database write batch size in bytes (default: 16777216)
    

    Please correct me if this isn’t what you meant.


    jonatack commented at 9:03 pm on April 16, 2025:

    Hm. Not sure how we can obtain -dbcache here. If not, as a fallback maybe indeed something like:

    0@@ -48,6 +48,7 @@
    1 #include <node/chainstatemanager_args.h>
    2+#include <node/coins_view_args.h>
    3 #include <node/context.h>
    4 #include <node/interface_ui.h>
    5 #include <node/kernel_notifications.h>
    6@@ -487,7 +488,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
    7     argsman.AddArg("-datadir=<dir>", "Specify data directory", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
    8-    argsman.AddArg("-dbbatchsize", "Maximum database write batch size in bytes (calculated dynamically by default)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
    9+    argsman.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (calculated per <...formula...>, default: %u with the default -dbcache of %u)", node::GetDbBatchSize(DEFAULT_DB_CACHE), DEFAULT_DB_CACHE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
    

    (Not sure I’m plugging in the correct constant in that example)


    l0rinc commented at 9:08 pm on April 16, 2025:
    Is this important? I would prefer not duplicating the logic in a comment. But it might be a good idea to log the calculated value based on the provided dbbatchsize - would that work for you instead?

    jonatack commented at 9:21 pm on April 16, 2025:

    Providing the default -dbbatchsize given the default -dbcache seems good to provide for an idea, while also informing that the calculation is partially based on the -dbcache value (unsure about “dynamically” as -dbcache is set on startup). Maybe something like

    0Maximum database write batch size in bytes (calculated using -dbcache, default: %u with the default -dbcache of %u)
    

    l0rinc commented at 10:18 am on April 17, 2025:

    Thanks, ended up with:

    0argsman.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: calculated from the provided `-dbcache` value or %u if none are provided)", node::GetDbBatchSize(DEFAULT_DB_CACHE)), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
    

    which prints:

    0./build/bin/bitcoind -dbcache=1000 -help-debug | grep -A2 dbbatchsize
    1  -dbbatchsize
    2       Maximum database write batch size in bytes (default: calculated from the
    3       provided `-dbcache` value or 16777216 if none are provided)
    

    What do you think?


    jonatack commented at 11:18 pm on April 18, 2025:

    LGTM, suggestion s/are/is/ and drop one of the two “provided"s

    “(default: calculated from the -dbcache value, or 16777216 if none is provided)”


    l0rinc commented at 6:25 am on April 19, 2025:
    I wrote “are”, since we can set two values which would override the default.

    l0rinc commented at 6:45 pm on April 19, 2025:
    Updated it and rebased to fix build - please re-review.

    jonatack commented at 5:03 pm on April 22, 2025:

    lgtm (thanks for updating)

    0$ ./build/bin/bitcoind -help-debug | grep -A2 dbbatchsize       
    1  -dbbatchsize
    2       Maximum database write batch size in bytes (default: calculated from the
    3       `-dbcache` value or 16777216 if not set)
    
  61. l0rinc force-pushed on Apr 16, 2025
  62. l0rinc force-pushed on Apr 17, 2025
  63. l0rinc force-pushed on Apr 17, 2025
  64. DrahtBot added the label CI failed on Apr 17, 2025
  65. DrahtBot commented at 10:37 am on April 17, 2025: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/40717139109

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  66. DrahtBot removed the label CI failed on Apr 17, 2025
  67. l0rinc force-pushed on Apr 19, 2025
  68. l0rinc renamed this:
    [IBD] flush UTXOs in bigger batches based on dbcache size
    [IBD] flush UTXO set in batches proportional to `dbcache` size
    on Apr 19, 2025
  69. in src/node/coins_view_args.h:24 in 8fd522b223 outdated
    17+{
    18+    return std::clamp(
    19+        (dbcache_bytes / DEFAULT_KERNEL_CACHE) * DEFAULT_DB_CACHE_BATCH,
    20+        /*lo=*/DEFAULT_DB_CACHE_BATCH,
    21+        /*hi=*/256_MiB
    22+    );
    


    jonatack commented at 4:51 pm on April 22, 2025:

    Might be good to add a comment for this magic value, i.e. (from the PR description):

    Capped at 256 MiB, as gains are barely measurable for bigger batches (see PR 31645)

    also, clang-format

    0-        /*hi=*/256_MiB
    1-    );
    2+        /*hi=*/256_MiB);
    

    and unneeded braces line 18

    0-        (dbcache_bytes / DEFAULT_KERNEL_CACHE) * DEFAULT_DB_CACHE_BATCH,
    1+        dbcache_bytes / DEFAULT_KERNEL_CACHE * DEFAULT_DB_CACHE_BATCH,
    

    l0rinc commented at 11:00 am on April 23, 2025:

    Capped at 256 MiB, as gains are barely measurable for bigger batches (see PR 31645)

    I don’t mind adding, but a simple blame would immediately reveal that

    unneeded braces line 18

    They may be implied, but I want to emphasize that mathematically this isn’t associative, i.e. not the same as

    0dbcache_bytes / (DEFAULT_KERNEL_CACHE * DEFAULT_DB_CACHE_BATCH)
    

    l0rinc commented at 0:15 am on August 7, 2025:
    Extracted min/max and added some comments
  70. in src/test/coins_tests.cpp:23 in 8fd522b223 outdated
    19@@ -20,6 +20,7 @@
    20 #include <vector>
    21 
    22 #include <boost/test/unit_test.hpp>
    23+#include <node/coins_view_args.h>
    


    jonatack commented at 4:53 pm on April 22, 2025:

    This seems to be the most frequently seen order:

    0 #include <coins.h>
    1+#include <node/coins_view_args.h>
    2 #include <streams.h>
    3@@ -20,7 +21,6 @@
    4 #include <vector>
    5 
    6 #include <boost/test/unit_test.hpp>
    7-#include <node/coins_view_args.h>
    8 
    9 using namespace util::hex_literals;
    

    l0rinc commented at 11:00 am on April 23, 2025:
    you mean we usually ignore the folder when sorting? I found examples for all sorts of includes (pun, intended) - I’ll adjust if I have to push again.

    hodlinator commented at 1:15 pm on April 23, 2025:

    you mean we usually ignore the folder when sorting?

    Think it’s a case of keeping our headers separate from external dependencies/libraries.


    l0rinc commented at 5:44 pm on April 23, 2025:
    Makes sense, will do next time I push

    l0rinc commented at 0:15 am on August 7, 2025:
    Done
  71. in src/node/coins_view_args.cpp:16 in 8fd522b223 outdated
    11@@ -10,7 +12,9 @@
    12 namespace node {
    13 void ReadCoinsViewArgs(const ArgsManager& args, CoinsViewOptions& options)
    14 {
    15-    if (auto value = args.GetIntArg("-dbbatchsize")) options.batch_write_bytes = *value;
    16-    if (auto value = args.GetIntArg("-dbcrashratio")) options.simulate_crash_ratio = *value;
    17+    if (const auto value = args.GetIntArg("-dbbatchsize")) options.batch_write_bytes = *value;
    18+    else options.batch_write_bytes = GetDbBatchSize(args.GetIntArg("-dbcache", DEFAULT_KERNEL_CACHE));
    


    jonatack commented at 4:55 pm on April 22, 2025:

    Feel free to ignore, but the following would be more readable and follow the most frequent convention in this codebase:

    0-    if (const auto value = args.GetIntArg("-dbbatchsize")) options.batch_write_bytes = *value;
    1-    else options.batch_write_bytes = GetDbBatchSize(args.GetIntArg("-dbcache", DEFAULT_KERNEL_CACHE));
    2+    if (const auto value = args.GetIntArg("-dbbatchsize")) {
    3+        options.batch_write_bytes = *value;
    4+    } else {
    5+        options.batch_write_bytes = GetDbBatchSize(args.GetIntArg("-dbcache", DEFAULT_KERNEL_CACHE));
    6+    }
    

    l0rinc commented at 11:00 am on April 23, 2025:
    Will do if I repush.

    l0rinc commented at 0:15 am on August 7, 2025:
    Added braces
  72. jonatack commented at 5:02 pm on April 22, 2025: member

    ACK 8fd522b223fc1405e70ca93c7e2d5a39f3f826fb

    A few nits, feel free to pick/choose/ignore.

  73. DrahtBot requested review from ryanofsky on Apr 22, 2025
  74. l0rinc commented at 6:38 pm on May 7, 2025: contributor
    Now that #30611 is merged, I’m drafting this PR until #32414 is also merged, since they eliminate the max memory usecase I’ve been optimizing for - and I have to remeasure the usecases to see if this is still the optimum.
  75. l0rinc marked this as a draft on May 7, 2025
  76. luke-jr commented at 4:14 pm on May 29, 2025: member
    I’m not sure it makes sense to adjust this based on dbcache size. Won’t a given batch size use the same amount of memory regardless of the size of the dbcache?
  77. l0rinc commented at 4:30 pm on May 29, 2025: contributor

    Won’t a given batch size use the same amount of memory regardless of the size of the dbcache

    It would, the assumption was that the user should be able to signal how much leftover memory they have - if they start the app with dbcache of 40MiB, allocating an extra 16MiB can be acceptable, but allocating an extra 64MiB can push the node over the edge. Even though we’re not (yet?) preallocating the batch string, doubling the size to accommodate the content would end up with a similar size. However, if you’re starting with a dbcache of 30GiB, you signal that even 256MiB of extra memory is fine. I.e. there should be a way to lower memory as much as possible, which wouldn’t be the case if the batch size were fixed.

    Note, however, that this has changed slightly with the merge of #30611, since there’s no significant difference between flushing with a dbcache of 4.5GiB and one with 45GiB (since we’re flushing regularly now).

    I’m open to suggestions for which direction to take from here.

  78. luke-jr referenced this in commit 2104df3209 on Jun 6, 2025
  79. DrahtBot added the label CI failed on Jun 15, 2025
  80. DrahtBot removed the label CI failed on Jun 18, 2025
  81. l0rinc force-pushed on Jul 30, 2025
  82. l0rinc marked this as ready for review on Jul 31, 2025
  83. l0rinc commented at 4:14 am on July 31, 2025: contributor

    Rebased, the PR is ready for review again!

    The batch size for UTXO set writes is now calculated based on the maximum dbcache size to ensure that with the default values, memory usage doesn’t increase, while reducing flushing time when there is enough memory available. The change reduces the IBD time by a fixed amount, it’s speeding up a critical part of saving the state for long-term storage.

  84. l0rinc force-pushed on Aug 7, 2025
  85. l0rinc commented at 0:34 am on August 7, 2025: contributor

    Pushed the remaining nits that I promised a long time ago :)

    Re-rebased, you can re-review with git range-diff af653f3...956a6b4

  86. in src/node/coins_view_args.h:19 in 956a6b4547 outdated
    15+namespace node
    16+{
    17+static constexpr size_t GetDbBatchSize(const size_t dbcache_bytes)
    18+{
    19+    const auto [min, max]{std::pair{DEFAULT_DB_CACHE_BATCH, 256_MiB}};
    20+    assert(min < max); // std::clamp is undefined if `lo` is greater than `hi`
    


    jonatack commented at 0:24 am on August 8, 2025:

    In 6fa584fde601dd8fc966fa2496084203c9503215, min can be equal to max (and perhaps align use of min/max vs lo/hi)

    0    assert(min <= max); // std::clamp is undefined if `min` is greater than `max`
    

    l0rinc commented at 1:32 am on August 8, 2025:
    • the lo and hi refer to the clamp argument names
    • if min == max the whole method could be eliminated
  87. in src/kernel/caches.h:21 in 956a6b4547 outdated
    15@@ -16,6 +16,9 @@ static constexpr size_t MAX_BLOCK_DB_CACHE{2_MiB};
    16 //! Max memory allocated to coin DB specific cache (bytes)
    17 static constexpr size_t MAX_COINS_DB_CACHE{8_MiB};
    18 
    19+//! The batch size of DEFAULT_KERNEL_CACHE
    20+static constexpr size_t DEFAULT_DB_CACHE_BATCH{16_MiB};
    21+
    


    jonatack commented at 0:25 am on August 8, 2025:

    In 6fa584fde601dd8fc966fa2496084203c9503215, seems it would make sense to place these two together.

     0  //! Suggested default amount of cache reserved for the kernel (bytes)
     1 static constexpr size_t DEFAULT_KERNEL_CACHE{450_MiB};
     2+//! Batch size of DEFAULT_KERNEL_CACHE
     3+static constexpr size_t DEFAULT_DB_CACHE_BATCH{16_MiB};
     4 //! Max memory allocated to block tree DB specific cache (bytes)
     5 static constexpr size_t MAX_BLOCK_DB_CACHE{2_MiB};
     6 //! Max memory allocated to coin DB specific cache (bytes)
     7 static constexpr size_t MAX_COINS_DB_CACHE{8_MiB};
     8 
     9-//! The batch size of DEFAULT_KERNEL_CACHE
    10-static constexpr size_t DEFAULT_DB_CACHE_BATCH{16_MiB};
    11-
    12 namespace kernel {
    13 struct CacheSizes {
    

    l0rinc commented at 1:33 am on August 8, 2025:
    We could, I’ll do it next time I push
  88. jonatack commented at 0:42 am on August 8, 2025: member

    ACK 956a6b454704120ba4c482d7157db89c20130e75

    • Note that #31645 (comment) refers to a change of approach from several months ago, prior to my previous review – in that context, I found the comment confusing
    • I would suggest not rebasing if there is no merge conflict; this eases reviewing the diff (and after, I’ll usually rebase the PR branch to master locally anyway)
    • Happy to re-ack if you take the review suggestions
  89. in src/test/coins_tests.cpp:1093 in 956a6b4547 outdated
    1084@@ -1085,4 +1085,26 @@ BOOST_AUTO_TEST_CASE(coins_resource_is_used)
    1085     PoolResourceTester::CheckAllDataAccountedFor(resource);
    1086 }
    1087 
    1088+BOOST_AUTO_TEST_CASE(db_batch_sizes)
    1089+{
    1090+    BOOST_REQUIRE_EQUAL(node::GetDbBatchSize(DEFAULT_KERNEL_CACHE), DEFAULT_DB_CACHE_BATCH);
    1091+    BOOST_REQUIRE_EQUAL(node::GetDbBatchSize(0_MiB), DEFAULT_DB_CACHE_BATCH);
    1092+
    1093+    BOOST_CHECK_EQUAL(node::GetDbBatchSize(4_MiB), 16'777'216);
    


    hodlinator commented at 7:34 pm on August 14, 2025:

    nit: Could motivate the min value:

    0    static_assert(MIN_DB_CACHE == 4_MiB);
    1    BOOST_CHECK_EQUAL(node::GetDbBatchSize(4_MiB), 16'777'216);
    
  90. in src/test/coins_tests.cpp:1108 in 956a6b4547 outdated
    1103+    BOOST_CHECK_EQUAL(node::GetDbBatchSize(4500_MiB), 167'772'160);
    1104+    BOOST_CHECK_EQUAL(node::GetDbBatchSize(7000_MiB), 251'658'240);
    1105+    BOOST_CHECK_EQUAL(node::GetDbBatchSize(10000_MiB), 268'435'456);
    1106+    BOOST_CHECK_EQUAL(node::GetDbBatchSize(45000_MiB), 268'435'456);
    1107+#endif
    1108+}
    


    hodlinator commented at 7:45 pm on August 14, 2025:

    Might as well make use of constexpr?

     0// Verify DB batch sizes:
     1static_assert(node::GetDbBatchSize(DEFAULT_KERNEL_CACHE) == DEFAULT_DB_CACHE_BATCH);
     2static_assert(node::GetDbBatchSize(0_MiB) == DEFAULT_DB_CACHE_BATCH);
     3
     4static_assert(node::GetDbBatchSize(4_MiB) == 16'777'216);
     5static_assert(node::GetDbBatchSize(10_MiB) == 16'777'216);
     6static_assert(node::GetDbBatchSize(45_MiB) == 16'777'216);
     7static_assert(node::GetDbBatchSize(100_MiB) == 16'777'216);
     8static_assert(node::GetDbBatchSize(450_MiB) == 16'777'216);
     9static_assert(node::GetDbBatchSize(1000_MiB) == 33'554'432);
    10static_assert(node::GetDbBatchSize(2000_MiB) == 67'108'864);
    11static_assert(node::GetDbBatchSize(3000_MiB) == 100'663'296);
    12
    13#if SIZE_MAX > UINT32_MAX
    14static_assert(node::GetDbBatchSize(4500_MiB) == 167'772'160);
    15static_assert(node::GetDbBatchSize(7000_MiB) == 251'658'240);
    16static_assert(node::GetDbBatchSize(10000_MiB) == 268'435'456);
    17static_assert(node::GetDbBatchSize(45000_MiB) == 268'435'456);
    18#endif
    

    l0rinc commented at 9:15 pm on August 14, 2025:
    I’m not a fan of compile time tests, they’re usually harder to debug, when needed. And these are as fast as they get, we wouldn’t be saving any time. So it would be inconsistent with other tests, while being slightly harder to debug and not any faster. Do you think there’s any tangible advantage there?

    hodlinator commented at 2:05 pm on August 18, 2025:

    I would just default to testing at compile time:

    • No need to (re)run ctest/test_bitcoin to exercise checks.
    • Only run when compilation unit is (re)built. Not re-run when iterating on test code in other compilation units.
    • Inconsistency might push other tests towards being converted to compile time, which I would say is a positive effect.

    l0rinc commented at 4:56 pm on August 25, 2025:
    These are extremely fast tests, keeping the usual test format has more advantages in my opinion. If you have a string preference or if other reviewers prefer that, I don’t mind changing, but I don’t think the current one has measurable disadvantages compared to the suggestion - while being more in-line with how we usually test, making GetDbBatchSize easily debuggable (often helps with understanding if you can step through it).
  91. in src/init.cpp:488 in 956a6b4547 outdated
    484@@ -484,7 +485,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
    485     argsman.AddArg("-coinstatsindex", strprintf("Maintain coinstats index used by the gettxoutsetinfo RPC (default: %u)", DEFAULT_COINSTATSINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
    486     argsman.AddArg("-conf=<file>", strprintf("Specify path to read-only configuration file. Relative paths will be prefixed by datadir location (only useable from command line, not configuration file) (default: %s)", BITCOIN_CONF_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
    487     argsman.AddArg("-datadir=<dir>", "Specify data directory", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
    488-    argsman.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: %u)", nDefaultDbBatchSize), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
    489+    argsman.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: calculated from the `-dbcache` value or %u if not set)", node::GetDbBatchSize(DEFAULT_DB_CACHE)), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
    


    hodlinator commented at 7:50 pm on August 14, 2025:

    Curious what made you decide on the current version vs:

    0    argsman.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: calculated from the `-dbcache` value or %u if not set)", DEFAULT_DB_CACHE_BATCH), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
    

    ?

    For the commits I would have leaned towards:

    • Only doing the rename + moving the constant (nDefaultDbBatchSize -> DEFAULT_DB_CACHE_BATCH) in the first.
    • Introducing GetDbBatchSize() and the rest in the second.

    But if you have a strong preference for using GetDbBatchSize() for the default value here that would mean touching the line twice which is annoying.


    l0rinc commented at 9:17 pm on August 14, 2025:
    This was specifically requested by @jonatack in #31645 (review). I personally don’t really mind either way.
  92. hodlinator commented at 8:02 pm on August 14, 2025: contributor

    Concept ACK 956a6b454704120ba4c482d7157db89c20130e75

    Have not confirmed the improvements in IBD speedup.

  93. hodlinator approved
  94. hodlinator commented at 12:29 pm on August 25, 2025: contributor

    ACK 956a6b454704120ba4c482d7157db89c20130e75

    Change scales hidden -dbbatchsize by the -dbcache setting if unset.

    Compared IBD until block 910'000 from local peer in 3 variations (both nodes on SSD, edit: both on NVMe); PR change for -dbcache of 450 vs 45'000, and base commit for PR with 45'000.

    • Baseline of -dbcache=450 averaged 267mins across 3 runs.
    • -dbcache=45000 without this PR averaged 241mins across 3 runs (these were probably the runs where I was doing the least work on the other node).
    • -dbcache=45000 with this PR averaged 233mins across 3 runs.

    That looks like at least an average 3% improvement in the -dbcache=45000 case for me. Still nice.

    Edit: Did another run with PR and -dbcache=45000 and without working on any of the nodes, got a wall time of 218min. Strengthening the impression that this PR is beneficial.

    With PR changes (956a6b454704120ba4c482d7157db89c20130e75):

     0rm -rf ~/.bitcoin && time ./build/bin/bitcoind -connect=workstation.lan -dbcache=45000 -stopatheight=910000
     1
     2real	238m57.330s
     3user	527m48.667s
     4sys	19m49.001s
     5
     6rm -rf ~/.bitcoin && time ./build/bin/bitcoind -connect=workstation.lan -dbcache=450 -stopatheight=910000
     7
     8real	266m16.473s
     9user	636m6.859s
    10sys	32m59.252s
    11
    12repeated:
    13
    14real	273m25.548s
    15user	640m5.442s
    16sys	31m58.674s
    17
    18rm -rf ~/.bitcoin && time ./build/bin/bitcoind -connect=workstation.lan -dbcache=45000 -stopatheight=910000
    19
    20real	228m12.943s
    21user	509m52.682s
    22sys	17m10.000s
    23
    24rm -rf ~/.bitcoin && time ./build/bin/bitcoind -connect=workstation.lan -dbcache=450 -stopatheight=910000
    25
    26real	261m47.071s
    27user	631m42.243s
    28sys	31m34.241s
    

    PR base (d767503b6a2618e0c99407acf98f3bd19fb7defd):

     0rm -rf ~/.bitcoin && time ./build/bin/bitcoind -connect=workstation.lan -dbcache=45000 -stopatheight=910000
     1
     2real	246m56.108s
     3user	522m46.642s
     4sys	20m24.343s
     5
     6again:
     7
     8real	232m41.467s
     9user	508m7.763s
    10sys	19m16.589s
    11
    12again:
    13
    14real	242m17.316s
    15user	506m25.625s
    16sys	18m45.029s
    

    => 241

    Again on PR:

    0rm -rf ~/.bitcoin && time ./build/bin/bitcoind -connect=workstation.lan -dbcache=45000 -stopatheight=910000
    1
    2real	232m37.784s
    3user	490m58.944s
    4sys	18m3.817s
    

    Averages:

    0450 runs:
    1266+273+262 = 267mins
    2
    345'000 without PR runs:
    4247+233+242 / 3 = 241min
    5
    645'000 with PR runs:
    7239+228+233 / 3 = 233min
    
  95. refactor: Extract default batch size into kernel
    The constant for the default batch size is moved to `kernel/caches.h` to consolidate kernel-related cache constants.
    8bbb7b8bf8
  96. coins: increase default `dbbatchsize` to 32 MiB
    The default database write batch size is increased from 16 MiB to 32 MiB to improve I/O efficiency and performance during UTXO flushes, particularly during Initial Block Download and `assumeutxo` loads.
    
    On systems with slower I/O, a larger batch size reduces overhead from numerous small writes. Measurements show this change provides a modest performance improvement on most hardware during a critical section, with a minimal peak memory increase (approx. 75 MiB on default settings).
    b6f8c48946
  97. l0rinc renamed this:
    [IBD] flush UTXO set in batches proportional to `dbcache` size
    [IBD] coins: increase default UTXO flush batch size to 32 MiB
    on Aug 28, 2025
  98. l0rinc force-pushed on Aug 28, 2025
  99. l0rinc commented at 5:23 pm on August 28, 2025: contributor

    Thanks a lot for testing this @hodlinator and @Eunovo.

    It seems that different configurations behave differently here, so I’ve retested a few scenarios on a few different platforms to understand what the performance increase depends on exactly. It’s not even a linear speedup (i.e. sometimes 32 MiB is faster, sometimes it’s 64 MiB), some dbcache values reliably produce a lot faster results, while others reliably produce slower results for the same config (i.e. the before-after-plot for different dbcache values doesn’t look like two parallel lines).


    My recent measurement used assumeutxo loading as a proxy (previous measurements indicated that it’s a realistic proxy for dumping utxos), and I ran each independent dbcache size 5 times to have proper error bars.

    First, on raspberry pi the difference is obviously a lot better with a bigger batch, even with a tiny memory increase:

    bitcoin_benchmark_comparison-batch_pi

     0dbcache=500 MB (dynamic batch size would be 17.8 MB)
     1  371bece67f: 2215.101±198.228 s  use fixed 64_MiB
     2  0a77b9eee6: 2698.961±108.559 s  use fixed 32_MiB
     3  1c88f34884: 2880.934±137.068 s  coins: introduce dynamic batch size calculator based on `dbcache` value
     4  6ca6f3b37b: 3513.171±761.674 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
     5
     6dbcache=2500 MB (dynamic batch size would be 88.9 MB)
     7  0a77b9eee6: 2202.481±100.896 s  use fixed 32_MiB
     8  6ca6f3b37b: 2277.482±131.931 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
     9  1c88f34884: 2313.098±202.818 s  coins: introduce dynamic batch size calculator based on `dbcache` value
    10  371bece67f: 2360.670±73.818 s  use fixed 64_MiB
    

    For a relatively performant i7 processor with HDD, I’m getting values are all over the place, but it’s obvious that the current batch size isn’t the best. Looks like dynamic sizing isn’t as performant as powers-of-two. The 32 MiB version seems pretty good though.

    bitcoin_benchmark_comparison-batch_i7

     0dbcache=500 MB (dynamic batch size would be 17.8 MB)
     1  371bece67f: 723.753±74.142 s  use fixed 64_MiB
     2  0a77b9eee6: 872.229±19.075 s  use fixed 32_MiB
     3  6ca6f3b37b: 924.901±34.220 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
     4  1c88f34884: 927.929±20.000 s  coins: introduce dynamic batch size calculator based on `dbcache` value
     5
     6dbcache=1000 MB (dynamic batch size would be 35.6 MB)
     7  371bece67f: 763.976±18.272 s  use fixed 64_MiB
     8  0a77b9eee6: 780.121±21.817 s  use fixed 32_MiB
     9  6ca6f3b37b: 850.142±53.788 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
    10  1c88f34884: 865.503±48.942 s  coins: introduce dynamic batch size calculator based on `dbcache` value
    11
    12dbcache=1500 MB (dynamic batch size would be 53.3 MB)
    13  0a77b9eee6: 741.196±42.555 s  use fixed 32_MiB
    14  371bece67f: 753.405±23.264 s  use fixed 64_MiB
    15  1c88f34884: 835.422±16.123 s  coins: introduce dynamic batch size calculator based on `dbcache` value
    16  6ca6f3b37b: 841.494±27.479 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
    17
    18dbcache=2000 MB (dynamic batch size would be 71.1 MB)
    19  371bece67f: 726.012±25.313 s  use fixed 64_MiB
    20  6ca6f3b37b: 799.339±29.058 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
    21  1c88f34884: 820.236±29.547 s  coins: introduce dynamic batch size calculator based on `dbcache` value
    22  0a77b9eee6: 858.368±16.452 s  use fixed 32_MiB
    23
    24dbcache=2500 MB (dynamic batch size would be 88.9 MB)
    25  371bece67f: 707.112±55.839 s  use fixed 64_MiB
    26  1c88f34884: 749.380±35.179 s  coins: introduce dynamic batch size calculator based on `dbcache` value
    27  6ca6f3b37b: 768.356±33.643 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
    28  0a77b9eee6: 781.631±25.348 s  use fixed 32_MiB
    29
    30dbcache=3000 MB (dynamic batch size would be 106.7 MB)
    31  371bece67f: 704.683±36.839 s  use fixed 64_MiB
    32  0a77b9eee6: 749.494±14.230 s  use fixed 32_MiB
    33  6ca6f3b37b: 802.988±29.282 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
    34  1c88f34884: 812.413±14.338 s  coins: introduce dynamic batch size calculator based on `dbcache` value
    35
    36dbcache=3500 MB (dynamic batch size would be 124.4 MB)
    37  371bece67f: 747.208±31.524 s  use fixed 64_MiB
    38  0a77b9eee6: 756.109±9.717 s  use fixed 32_MiB
    39  1c88f34884: 769.228±35.585 s  coins: introduce dynamic batch size calculator based on `dbcache` value
    40  6ca6f3b37b: 783.391±43.066 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
    41
    42dbcache=4000 MB (dynamic batch size would be 142.2 MB)
    43  0a77b9eee6: 742.536±13.438 s  use fixed 32_MiB
    44  1c88f34884: 752.711±21.669 s  coins: introduce dynamic batch size calculator based on `dbcache` value
    45  6ca6f3b37b: 753.414±15.107 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
    46  371bece67f: 759.595±15.312 s  use fixed 64_MiB
    47
    48dbcache=4500 MB (dynamic batch size would be 160.0 MB)
    49  371bece67f: 722.305±22.894 s  use fixed 64_MiB
    50  0a77b9eee6: 738.573±28.200 s  use fixed 32_MiB
    51  1c88f34884: 743.053±12.117 s  coins: introduce dynamic batch size calculator based on `dbcache` value
    52  6ca6f3b37b: 747.821±16.638 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
    53
    54dbcache=5000 MB (dynamic batch size would be 177.8 MB)
    55  6ca6f3b37b: 707.499±23.626 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
    56  1c88f34884: 708.064±21.035 s  coins: introduce dynamic batch size calculator based on `dbcache` value
    57  0a77b9eee6: 713.495±18.264 s  use fixed 32_MiB
    58  371bece67f: 723.671±18.868 s  use fixed 64_MiB
    59
    60dbcache=5500 MB (dynamic batch size would be 195.6 MB)
    61  371bece67f: 697.942±15.469 s  use fixed 64_MiB
    62  0a77b9eee6: 705.869±15.561 s  use fixed 32_MiB
    63  6ca6f3b37b: 797.158±15.429 s  Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues
    64  1c88f34884: 797.677±17.496 s  coins: introduce dynamic batch size calculator based on `dbcache` value
    
    • I reran 32_MiB for dbcache=2000 and got the same result again, it doesn’t seem to be a measurement error

    Also measured on a very performant i9 with a really fast NVMe - making the difference between memory and background storage more blurry. Here a bigger dbcache doesn’t even increase the overall speed, assumeutxo is basically the same speed whatever we do - here the goal is to keep the current speed.

    bitcoin_benchmark_comparison-batch_i9

    dbcache=500 MB (dynamic batch size would be 17.8 MB) 371bece67f: 409.650±1.656 s use fixed 64_MiB 0a77b9eee6: 411.217±4.367 s use fixed 32_MiB 1c88f34884: 455.376±1.326 s coins: introduce dynamic batch size calculator based on dbcache value 6ca6f3b37b: 455.975±1.697 s Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues

    dbcache=1000 MB (dynamic batch size would be 35.6 MB) 371bece67f: 416.379±1.989 s use fixed 64_MiB 1c88f34884: 448.944±6.389 s coins: introduce dynamic batch size calculator based on dbcache value 0a77b9eee6: 449.627±1.714 s use fixed 32_MiB 6ca6f3b37b: 456.552±27.444 s Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues

    dbcache=1500 MB (dynamic batch size would be 53.3 MB) 371bece67f: 400.644±1.372 s use fixed 64_MiB 6ca6f3b37b: 430.214±3.485 s Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues 1c88f34884: 430.987±4.601 s coins: introduce dynamic batch size calculator based on dbcache value 0a77b9eee6: 442.139±9.970 s use fixed 32_MiB

    dbcache=2000 MB (dynamic batch size would be 71.1 MB) 371bece67f: 435.944±2.428 s use fixed 64_MiB 6ca6f3b37b: 443.443±3.007 s Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues 1c88f34884: 446.855±2.847 s coins: introduce dynamic batch size calculator based on dbcache value 0a77b9eee6: 467.190±4.509 s use fixed 32_MiB

    dbcache=2500 MB (dynamic batch size would be 88.9 MB) 6ca6f3b37b: 431.099±1.968 s Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues 1c88f34884: 431.577±3.782 s coins: introduce dynamic batch size calculator based on dbcache value 371bece67f: 435.850±9.648 s use fixed 64_MiB 0a77b9eee6: 461.795±4.254 s use fixed 32_MiB

    dbcache=3000 MB (dynamic batch size would be 106.7 MB) 6ca6f3b37b: 428.011±3.228 s Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues 1c88f34884: 429.848±2.192 s coins: introduce dynamic batch size calculator based on dbcache value 371bece67f: 431.654±5.198 s use fixed 64_MiB 0a77b9eee6: 445.446±5.815 s use fixed 32_MiB

    dbcache=3500 MB (dynamic batch size would be 124.4 MB) 1c88f34884: 453.404±4.046 s coins: introduce dynamic batch size calculator based on dbcache value 6ca6f3b37b: 455.361±3.817 s Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues 371bece67f: 469.343±2.369 s use fixed 64_MiB 0a77b9eee6: 474.821±12.205 s use fixed 32_MiB

    dbcache=4000 MB (dynamic batch size would be 142.2 MB) 1c88f34884: 443.378±5.214 s coins: introduce dynamic batch size calculator based on dbcache value 6ca6f3b37b: 443.661±4.678 s Merge bitcoin/bitcoin#33241: Update libmultiprocess subtree to fix build issues 371bece67f: 457.182±5.747 s use fixed 64_MiB 0a77b9eee6: 471.365±8.131 s use fixed 32_MiB


    Edit: The above ones are the complete AssumeUTXO load & dump. Extracting only the batch saving times (the only change of the PR) on an i7 with HDD reveals a 40% speedup for default memory and a 3% speedup for bigger one with fewer batch writes:

     0COMMITS="8bbb7b8bf8e3b2b6465f318ec102cc5275e5bf8c b6f8c48946cbfceb066de660c485ae1bd2c27cc1"; \
     1CC=gcc; CXX=g++; \
     2BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/ShallowBitcoinData"; LOG_DIR="$BASE_DIR/logs"; UTXO_SNAPSHOT_PATH="$BASE_DIR/utxo-880000.dat"; \
     3(echo ""; for c in $COMMITS; do git fetch -q origin $c && git log -1 --pretty='%h %s' $c || exit 1; done; echo "") && \
     4for DBCACHE in 450 4500; do \
     5  hyperfine \
     6  --sort command \
     7  --runs 5 \
     8  --export-json "$BASE_DIR/assumeutxo-$(sed -E 's/(\w{8})\w+ ?/\1-/g;s/-$//'<<<"$COMMITS")-$DBCACHE-$CC-$(date +%s).json" \
     9  --parameter-list COMMIT ${COMMITS// /,} \
    10  --prepare "killall bitcoind 2>/dev/null; rm -rf $DATA_DIR/chainstate $DATA_DIR/chainstate_snapshot $DATA_DIR/debug.log; git checkout {COMMIT}; git clean -fxd; git reset --hard && \
    11    cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_IPC=OFF && ninja -C build bitcoind bitcoin-cli && \
    12      ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=1 -printtoconsole=0; sleep 20 && \
    13      ./build/bin/bitcoind -datadir=$DATA_DIR -daemon -blocksonly -connect=0 -dbcache=$DBCACHE -printtoconsole=0; sleep 20" \
    14    --cleanup "cp $DATA_DIR/debug.log $LOG_DIR/debug-assumeutxo-{COMMIT}-dbcache-$DBCACHE-$(date +%s).log; \
    15      build/bin/bitcoin-cli -datadir=$DATA_DIR stop || true; killall bitcoind || true" \
    16    "COMPILER=$CC DBCACHE=$DBCACHE ./build/bin/bitcoin-cli -datadir=$DATA_DIR -rpcclienttimeout=0 loadtxoutset $UTXO_SNAPSHOT_PATH"; \
    17done
    18
    198bbb7b8bf8 refactor: Extract default batch size into kernel
    20b6f8c48946 coins: increase default `dbbatchsize` to 32 MiB
    21
    22Benchmark 1: COMPILER=gcc DBCACHE=450 ./build/bin/bitcoin-cli -datadir=/mnt/my_storage/ShallowBitcoinData -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-880000.dat (COMMIT = 8bbb7b8bf8e3b2b6465f318ec102cc5275e5bf8c)
    23  Time (mean ± σ):     993.753 s ± 41.940 s    [User: 0.002 s, System: 0.001 s]
    24  Range (min  max):   920.460 s  1027.298 s    5 runs
    25 
    26Benchmark 2: COMPILER=gcc DBCACHE=450 ./build/bin/bitcoin-cli -datadir=/mnt/my_storage/ShallowBitcoinData -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-880000.dat (COMMIT = b6f8c48946cbfceb066de660c485ae1bd2c27cc1)
    27  Time (mean ± σ):     827.580 s ± 37.633 s    [User: 0.001 s, System: 0.002 s]
    28  Range (min  max):   778.174 s  868.777 s    5 runs
    29 
    30Relative speed comparison
    31        1.20 ±  0.07  COMPILER=gcc DBCACHE=450 ./build/bin/bitcoin-cli -datadir=/mnt/my_storage/ShallowBitcoinData -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-880000.dat (COMMIT = 8bbb7b8bf8e3b2b6465f318ec102cc5275e5bf8c)
    32        1.00          COMPILER=gcc DBCACHE=450 ./build/bin/bitcoin-cli -datadir=/mnt/my_storage/ShallowBitcoinData -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-880000.dat (COMMIT = b6f8c48946cbfceb066de660c485ae1bd2c27cc1)
    33
    34
    35Benchmark 1: COMPILER=gcc DBCACHE=4500 ./build/bin/bitcoin-cli -datadir=/mnt/my_storage/ShallowBitcoinData -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-880000.dat (COMMIT = 8bbb7b8bf8e3b2b6465f318ec102cc5275e5bf8c)
    36  Time (mean ± σ):     754.283 s ±  6.920 s    [User: 0.001 s, System: 0.002 s]
    37  Range (min  max):   747.235 s  765.162 s    5 runs
    38 
    39Benchmark 2: COMPILER=gcc DBCACHE=4500 ./build/bin/bitcoin-cli -datadir=/mnt/my_storage/ShallowBitcoinData -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-880000.dat (COMMIT = b6f8c48946cbfceb066de660c485ae1bd2c27cc1)
    40  Time (mean ± σ):     740.267 s ±  9.414 s    [User: 0.001 s, System: 0.002 s]
    41  Range (min  max):   726.094 s  751.329 s    5 runs
    42 
    43Relative speed comparison
    44        1.02 ±  0.02  COMPILER=gcc DBCACHE=4500 ./build/bin/bitcoin-cli -datadir=/mnt/my_storage/ShallowBitcoinData -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-880000.dat (COMMIT = 8bbb7b8bf8e3b2b6465f318ec102cc5275e5bf8c)
    45        1.00          COMPILER=gcc DBCACHE=4500 ./build/bin/bitcoin-cli -datadir=/mnt/my_storage/ShallowBitcoinData -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-880000.dat (COMMIT = b6f8c48946cbfceb066de660c485ae1bd2c27cc1)
    

    Edit2: A similar measurement on a Raspberry PI for only the dumping part of the UTXOs reveals a 70% speedup (before: ~39 minutes vs ~23 minutes after) for default dbcache (consisting of 58 separate batch writes).


    So in summary, the dynamic scaling seems like a needless complication and 64 MiB seems to add too much extra memory.

    The above tests indicate that a constant 32 MiB bump (without any differentiation based on dbcache) is usually faster for most system with most dbcache values than the current one of 16 MiB (even if usually not as fast as the 64 MiB alternative).

    Comparing the massif memory usage of the current 16 MiB:

     0    MB
     1744.4^                            :
     2     |# :::: :  ::: ::  :::::  ::@:::::   ::::::::@@
     3     |# : :: :  ::: ::  : ::   ::@:: ::   ::: ::: @
     4     |#:: :: :  ::: ::  : ::   ::@:: ::   ::: ::: @
     5     |#:: :: :  ::: ::  : ::   ::@:: :: ::::: ::: @
     6     |#:: :: :  ::: ::  : ::   ::@:: :: : ::: ::: @
     7     |#:: :: :  ::: ::  : ::   ::@:: :::: ::: ::: @
     8     |#:: :: :::::: ::  : ::   ::@:: :::: ::: ::: @
     9     |#:: ::::: ::: ::::: :: ::::@:: :::: ::: ::: @
    10     |#:: ::::: ::::::: : :: : ::@:: :::: ::: ::: @
    11     |#:: ::::: ::::::: : :: : ::@:: :::: ::: ::: @
    12     |#:: ::::: ::::::: : :: : ::@:: :::: ::: ::: @
    13     |#:: ::::: ::::::: : :: : ::@:: :::: ::: ::: @
    14     |#:: ::::: ::::::: : :: : ::@:: :::: ::: ::: @
    15     |#:: ::::: ::::::: : :: : ::@:: :::: ::: ::: @ ::::::::@@::@::::::::::@::
    16     |#:: ::::: ::::::: : :: : ::@:: :::: ::: ::: @ : : : : @ ::@:::: :::::@::
    17     |#:: ::::: ::::::: : :: : ::@:: :::: ::: ::: @ : : : : @ ::@:::: :::::@::
    18     |#:: ::::: ::::::: : :: : ::@:: :::: ::: ::: @ : : : : @ ::@:::: :::::@::
    19     |#:: ::::: ::::::: : :: : ::@:: :::: ::: ::: @ : : : : @ ::@:::: :::::@::
    20     |#:: ::::: ::::::: : :: : ::@:: :::: ::: ::: @ : : : : @ ::@:::: :::::@::
    21   0 +----------------------------------------------------------------------->h
    22     0                                                                   1.603
    

    vs the new proposed fix 32 MiB:

     0    MB
     1819.6^      ##
     2     | :    #       :      :  :           :
     3     | :  ::# :::@@:::::::::  :  @:::::: :::::::
     4     | :  : # :: @ ::: :: ::  :  @:: ::  :::: :
     5     | :: : # :: @ ::: :: ::  :  @:: ::  :::: :
     6     | :: : # :: @ ::: :: ::  :  @:: ::  :::: :
     7     | :: : # :: @ ::: :: ::  :  @:: ::  :::: :
     8     | :::: # :: @ ::: :: ::  :  @:: ::  :::: :
     9     | :::: # :: @ ::: :: ::  :  @:: ::  :::: :
    10     | :::: # :: @ ::: :: :::::  @:: ::  :::: :
    11     | :::: # :: @ ::: :: ::: :::@:: ::  :::: :
    12     | :::: # :: @ ::: :: ::: :: @:: ::  :::: :
    13     | :::: # :: @ ::: :: ::: :: @:: :: ::::: :
    14     | :::: # :: @ ::: :: ::: :: @:: :: ::::: :
    15     | :::: # :: @ ::: :: ::: :: @:: :: ::::: : :::::::::::@::::@::::@:::::::@
    16     | :::: # :: @ ::: :: ::: :: @:: :: ::::: : : : :::::: @::::@::::@:::::::@
    17     | :::: # :: @ ::: :: ::: :: @:: :: ::::: : : : :::::: @::::@::::@:::::::@
    18     | :::: # :: @ ::: :: ::: :: @:: :: ::::: : : : :::::: @::::@::::@:::::::@
    19     | :::: # :: @ ::: :: ::: :: @:: :: ::::: : : : :::::: @::::@::::@:::::::@
    20     | :::: # :: @ ::: :: ::: :: @:: :: ::::: : : : :::::: @::::@::::@:::::::@
    21   0 +----------------------------------------------------------------------->h
    22     0                                                                   1.484
    

    Reveals that we’re still a lot below 1GiB, the peak memory only increased by 75 MiB in total (and only during dumping the UTXO set), while the total time decreased from 1.6h to 1.48h (8% faster).

    I was interested in the total memory usage of dbcache=4500 as well:

     0    GB
     14.705^                                 :
     2     | ##::  :::::  ::@:::  :@::: :@::::
     3     | # ::  : :::  ::@: :  :@::: :@: ::
     4     | # ::  : :::  ::@: :  :@::: :@: ::
     5     | # ::  : :::  ::@: :  :@::: :@: ::
     6     | # :: :: :::  ::@: : ::@::: :@: ::
     7     | # :: :: :::  ::@: : ::@::: :@: ::
     8     | # :: :: :::  ::@: : ::@::: :@: ::
     9     | # :: :: :::  ::@: : ::@::: :@: :::::::
    10     | # :: :: :::  ::@: : ::@::: :@: ::: ::
    11     | # :: :: ::: :::@: : ::@::: :@: ::: ::
    12     | # :: :: ::: :::@: : ::@::: :@: ::: ::
    13     | # :: :: ::: :::@: : ::@::: :@: ::: ::
    14     | # :: :: ::: :::@: : ::@::: :@: ::: ::
    15     | # ::::: ::: :::@: : ::@::: :@: ::: ::
    16     |@# ::::: ::: :::@: : ::@::: :@: ::: ::
    17     |@# ::::: :::@:::@: : ::@::: :@: ::: ::
    18     |@# ::::: :::@:::@: : ::@::: :@: ::: ::
    19     |@# ::::: :::@:::@: : ::@::: :@: ::: ::
    20     |@# ::::: :::@:::@: ::::@:::::@: ::: :: @@::@:::@:::::::::@::::::@::::::@
    21   0 +----------------------------------------------------------------------->h
    22     0                                                                   1.294
    

    vs master:

     0    GB
     14.640^                       ::
     2     |  #::  :::@@:  ::@:   :@ @::: @:::
     3     |  #:   :: @ :  ::@:  @:@ @ :: @: :
     4     |  #:   :: @ :  ::@:  @:@ @ :: @: :
     5     |  #:   :: @ :  ::@:  @:@ @ :::@: :
     6     |  #:  ::: @ : :::@:  @:@ @ :::@: :
     7     |  #:  ::: @ : :::@:  @:@ @ :::@: :
     8     |  #:  ::: @ : :::@:  @:@ @ :::@: :
     9     |  #:  ::: @ : :::@:  @:@ @ :::@: : :::
    10     |  #:  ::: @ : :::@:  @:@ @ :::@: : :::
    11     |  #:  ::: @ : :::@:  @:@ @ :::@: : :::
    12     |  #:  ::: @ : :::@:  @:@ @ :::@: : :::
    13     | :#:  ::: @ :@:::@:  @:@ @ :::@: : :::
    14     | :#:  ::: @ :@:::@:  @:@ @ :::@: :::::
    15     | :#:  ::: @ :@:::@:  @:@ @ :::@: :::::
    16     | :#:  ::: @ :@:::@:::@:@ @ :::@: :::::
    17     | :#:  ::: @ :@:::@:: @:@ @ :::@: :::::
    18     | :#:  ::: @ :@:::@:: @:@ @ :::@: :::::
    19     | :#: :::: @ :@:::@:: @:@ @ :::@: :::::
    20     | :#: :::: @ :@:::@:: @:@ @ :::@: :::::::::::::::::::::::::::::::::::@:::
    21   0 +----------------------------------------------------------------------->h
    22     0                                                                   1.338
    

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-09-07 09:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me