{
 "number": 35195,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35195",
 "title": "coins: cache UTXO outpoint hash codes",
 "author": "l0rinc",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-05-02T16:22:25Z",
 "updated_at": "2026-07-31T17:39:15Z",
 "age_days": 137,
 "draft": true,
 "labels": [
  "UTXO Db and Indexes",
  "Needs rebase"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "16e77fdf1324e7419eff5adb358f2c9b27db948a",
 "head_ref": "l0rinc/noexcept-false",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [],
 "additions": 21,
 "deletions": 7,
 "changed_files": 2,
 "commit_count": 2,
 "size_bucket": "S",
 "mergeable_state": "dirty",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "optout21",
      "url": "https://github.com/bitcoin/bitcoin/pull/35195#pullrequestreview-4379811751"
     }
    ]
   },
   "conflicts": [
    {
     "number": 35713,
     "title": "Remove boost as a unit test runner",
     "author": "rustaceanrob"
    },
    {
     "number": 35215,
     "title": "coins: use SipHash-1-3-UJ for CCoinsMap keys",
     "author": "l0rinc"
    }
   ]
  }
 },
 "acks_parsed": {
  "optout21": {
   "kind": "concept_ack",
   "hash": "16e77fdf1324e7419eff5adb358f2c9b27db948a",
   "t": "2026-05-28T10:02:14Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 1,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "optout21"
  ]
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": false,
  "mergeable_state": "dirty",
  "last_author_activity": "2026-05-02T15:31:08Z",
  "last_reviewer_activity": "2026-05-28T10:02:14Z",
  "last_reviewer": "optout21",
  "author_silent_days": 138,
  "waiting_on_author_days": 112,
  "days_since_update": 47
 },
 "refs": {
  "mentioned": [
   16957,
   31132
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 16957,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2019-09-30",
    "title": "9% less memory: make SaltedOutpointHasher noexcept"
   },
   {
    "number": 31132,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "validation: fetch block inputs on parallel threads"
   }
  ],
  "conflicts": [
   35713,
   35215
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/test/hash_tests.cpp",
  "src/util/hasher.h"
 ],
 "body": "**Problem:** `CCoinsMap` uses `SaltedOutpointHasher` for UTXO cache lookups. Since #16957 marked the hasher `noexcept`, `libstdc++` no longer stores cached hash codes in unordered-map nodes.\n\n#16957 optimized for memory: the original benchmark reported about `9.4%` lower peak RSS, but also about `1.6%` slower runtime from recomputing hashes. This PR takes the opposite side of that same tradeoff for the UTXO cache: allow `libstdc++` to store one cached hash code per unordered-map node, avoiding repeated `SipHash` work in rehashing and some table operations.\n\nBenchmarks consistently favored cached hash codes:\n* Full IBD/reindex-chainstate runs showed about 2-3% faster at both low and high `-dbcache`.\n* `AssumeUTXO` loading showed the strongest signal, up to about `11%` faster. This is the cleanest dbcache-exercising benchmark here because it avoids block download and full validation work, so the coins-cache effect is less diluted.\n* Compatibility measurements on top of the input-fetcher parallelization and `SipHash` optimization work still showed cached hash codes helping: about `3%` faster on an Intel N150 after the `SipHash` change, and still about `1%` faster on M4 with `-dbcache=10000`.\n* Theoretically, memory can increase on `libstdc++` by up to one cached `size_t` per node. In practice, recent Massif runs were roughly neutral at moderate dbcache values, while a very large dbcache run showed the expected higher peak.\n\nFor `CCoinsMap`, no allocator sizing change is needed since its [`PoolAllocator` block size](https://github.com/bitcoin/bitcoin/blob/ef499680c8d426ac04f3a5d4ad67cbb3426e0e21/src/coins.h#L215) already reserves implementation-defined node overhead and explicitly accounts for implementations where \"the hash value is stored as well\". That assumption remained in the code after the #16957 `noexcept` change disabled cached hash codes for this hasher, so there is no memory calculation to restore in this PR.\n\nThis is also consistent with concerns raised in the original [#16957](https://github.com/bitcoin/bitcoin/pull/16957) discussion: reviewers noted the `noexcept` change was slower, that it imposed a penalty when memory was not limiting, and that \"it might be possible to regain the performance loss by caching the hash ourselves.\" This PR lets `libstdc++` do that caching for us.\n\n**Fix:** Revert #16957's `SaltedOutpointHasher` `noexcept` change, then document the now-intentional `noexcept(false)` contract and add a focused unit test.\n\nThe implementation still does not throw; only the exception specification used by `libstdc++`'s unordered-map node policy changes: for a fast hash functor that may throw, `libstdc++` caches hash codes in nodes.\n\nThe test checks both inputs to that policy:\n* `SaltedOutpointHasher` is not nothrow-invocable\n* on `libstdc++`, it remains classified as a fast hash\n\nThis mirrors the type-level contract style used in [`libstdc++`'s own hash tests](https://github.com/gcc-mirror/gcc/blob/c0c911821bad196a2abb71e56564bf8c0dfcf231/libstdc%2B%2B-v3/testsuite/std/time/hash.cc#L62).\n\n**Reproducer:**\n\nFresh reindex-chainstate run\n\n```bash\nfor DBCACHE in 1000 30000; do \\\n    COMMITS=\"ef499680c8d426ac04f3a5d4ad67cbb3426e0e21 1016fba624f840fc893b8de61df4a149ac9551a8\"; \\\n    STOP=946649; CC=gcc; CXX=g++; \\\n    BASE_DIR=\"/mnt/my_storage\"; DATA_DIR=\"$BASE_DIR/BitcoinData\"; LOG_DIR=\"$BASE_DIR/logs\"; \\\n    (echo \"\"; for c in $COMMITS; do git fetch -q origin \"$c\" 2>/dev/null || true; git log -1 --pretty='%h %s' $c || exit 1; done) && \\\n    (echo \"\" && echo \"$(date -I) | reindex-chainstate | ${STOP} blocks | dbcache ${DBCACHE} | $(hostname) | $(uname -m) | $(lscpu | grep 'Model name' | head -1 | cut -d: -f2 | xargs) | $(nproc) cores | $(free -h | awk '/^Mem:/{print $2}') RAM | $(lsblk -no ROTA $(df --output=source $BASE_DIR | tail -1) | grep -q 1 && echo HDD || echo SSD)\"; echo \"\") && \\\n    hyperfine \\\n    --sort command \\\n    --runs 1 \\\n    --export-json \"$BASE_DIR/rdx-$(sed -E 's/[^ ]+/\\L&/g;s/[.]/_/g;s/ /-/g'<<<\"$COMMITS\")-$STOP-$DBCACHE-$CC.json\" \\\n    --parameter-list COMMIT ${COMMITS// /,} \\\n    --prepare \"killall -9 bitcoind 2>/dev/null; rm -f ./build/bin/bitcoind; git clean -fxd; git reset --hard {COMMIT} && \\\n      cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release && ninja -C build bitcoind -j1 && \\\n      ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=1000 -printtoconsole=0; sleep 20; rm -f $DATA_DIR/debug.log; rm -rfd $DATA_DIR/indexes;\" \\\n    --conclude \"killall bitcoind || true; sleep 5; grep -q 'height=0' $DATA_DIR/debug.log && grep -q 'Disabling script verification at block #1' $DATA_DIR/debug.log && grep -q 'height=$STOP' $DATA_DIR/debug.log && grep 'Bitcoin Core version' $DATA_DIR/debug.log | grep -q \\\"\\$(git rev-parse --short=12 {COMMIT})\\\"; \\\n                cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-$(date +%s).log\" \\\n    \"COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0\";\ndone\n\nef499680c8 Merge bitcoin/bitcoin#34176: wallet: crash fix, handle non-writable db directories\n1016fba624 util: allow caching outpoint hash codes\n\n2026-04-30 | reindex-chainstate | 946649 blocks | dbcache 1000 | i9-ssd | x86_64 | Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz | 16 cores | 62Gi RAM | SSD\n\nBenchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=946649 -dbcache=1000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = ef499680c8d426ac04f3a5d4ad67cbb3426e0e21)\n  Time (abs \u2261):        19288.792 s               [User: 33333.953 s, System: 1949.252 s]\n\nBenchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=946649 -dbcache=1000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = 1016fba624f840fc893b8de61df4a149ac9551a8)\n  Time (abs \u2261):        18968.171 s               [User: 33375.739 s, System: 2030.692 s]\n\nRelative speed comparison\n        1.02          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=946649 -dbcache=1000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = ef499680c8d426ac04f3a5d4ad67cbb3426e0e21)\n        1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=946649 -dbcache=1000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = 1016fba624f840fc893b8de61df4a149ac9551a8)\n\nef499680c8 Merge bitcoin/bitcoin#34176: wallet: crash fix, handle non-writable db directories\n1016fba624 util: allow caching outpoint hash codes\n\n2026-04-30 | reindex-chainstate | 946649 blocks | dbcache 30000 | i9-ssd | x86_64 | Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz | 16 cores | 62Gi RAM | SSD\n\nBenchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=946649 -dbcache=30000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = ef499680c8d426ac04f3a5d4ad67cbb3426e0e21)\n  Time (abs \u2261):        17615.001 s               [User: 24395.224 s, System: 771.559 s]\n\nBenchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=946649 -dbcache=30000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = 1016fba624f840fc893b8de61df4a149ac9551a8)\n  Time (abs \u2261):        17209.581 s               [User: 24079.479 s, System: 785.327 s]\n\nRelative speed comparison\n        1.02          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=946649 -dbcache=30000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = ef499680c8d426ac04f3a5d4ad67cbb3426e0e21)\n        1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=946649 -dbcache=30000 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = 1016fba624f840fc893b8de61df4a149ac9551a8)\n```\n\nEarlier supporting measurements\n\nOriginal #16957 result:\n\n```text\nmaster                                4:13:59   7696728 KiB\n2019-09-SaltedOutpointHasher-noexcept 4:18:11   6971412 KiB\nchange                                +1.65%    -9.42%\n```\n\n#16957 also showed that increasing dbcache on the `noexcept` branch could recover performance while staying below old memory use:\n\n```text\nmaster -dbcache=5000     4:13:59   7696728 KiB\nnoexcept -dbcache=5471   4:01:16   7282044 KiB\n```\n\nEarlier cached-hash measurements:\n\n```text\nIBD to height 909090, -dbcache=5000\nbaseline:     27046.236 s \u00b1 631.042 s\ncached hash:  26226.707 s \u00b1 373.829 s\nchange:       ~3.0% faster\n\nreindex-chainstate to height 909090, -dbcache=5000\nbaseline:     27728.594 s \u00b1 96.850 s\ncached hash:  26847.390 s \u00b1 105.590 s\nchange:       ~3.2% faster\n\nloadtxoutset, -dbcache=500\nbaseline:     451.699 s \u00b1 1.126 s\ncached hash:  422.857 s \u00b1 1.488 s\nchange:       ~6.4% faster\n\nloadtxoutset, -dbcache=3000\nbaseline:     426.088 s \u00b1 1.824 s\ncached hash:  403.923 s \u00b1 1.043 s\nchange:       ~5.2% faster\n\nloadtxoutset, -dbcache=4000\nbaseline:     440.708 s \u00b1 2.242 s\ncached hash:  423.934 s \u00b1 4.145 s\nchange:       ~3.8% faster\n```\n\nLater `noexcept` removal measurements:\n\n```text\nreindex-chainstate, height 916000, -dbcache=15000\nbaseline:     23856.269 s\nno noexcept:  22734.025 s\nchange:       ~4.7% faster\n\nreindex-chainstate, height 917000, -dbcache=30000\nbaseline:     12703.654 s\nno noexcept:  11957.649 s\nchange:       ~5.9% faster\n\nassumeutxo on i9 SSD, -dbcache=450\nbaseline:     347.241 s \u00b1 3.282 s\nno noexcept:  309.558 s \u00b1 1.951 s\nchange:       ~10.9% faster\n```\n\nMemory observations:\n\n```text\nOriginal #16957:\nmaster:      7696728 KiB\nnoexcept:    6971412 KiB\nchange:      -9.42%\n\nRecent Massif, assumeutxo -dbcache=450:\nbaseline:     744.5 MB\ncached hash:  737.7 MB\n\nRecent Massif, assumeutxo -dbcache=4500:\nbaseline:     4.640 GB\ncached hash:  4.641 GB\n\nVery large dbcache:\nbaseline:     24.11 GB\ncached hash:  25.49 GB\n```\n\nCompatibility with input-fetcher and `SipHash` work\n\nThis change has been in my queue for more than a year, so some supporting measurements are older and were collected while adjacent input-fetcher and `SipHash` work was still evolving. The relevant compatibility measurements are documented in [this #31132 review comment](https://github.com/bitcoin/bitcoin/pull/31132#pullrequestreview-4179154342).\n\nAfter input fetching, `SipHash` inlining, and `noexcept` removal on an Intel N150:\n\n```text\nreindex-chainstate, height 943349, -dbcache=1000, Intel N150\n\ninput fetcher baseline:     20014.228 s\n`SipHash` optimized:          18933.341 s\nno noexcept:                18388.590 s\n\nchange from `SipHash` optimized to no noexcept: ~2.9% faster\nchange from input fetcher baseline to no noexcept: ~8.1% faster\n```\n\nAfter input fetching, `SipHash` inlining, and `noexcept` removal on an M4 Max:\n\n```text\nreindex-chainstate, height 943349, -dbcache=10000, Apple M4 Max\n\ninput fetcher baseline:     4836.390 s\n`SipHash` optimized:          4543.009 s\nno noexcept:                4493.407 s\n\nchange from `SipHash` optimized to no noexcept: ~1.1% faster\nchange from input fetcher baseline to no noexcept: ~7.1% faster\n```\n\nThese measurements suggest cached hash-code nodes remain useful even when the hasher itself gets faster and block input fetching is parallelized.",
 "commits": [
  {
   "sha": "fb0c20756768c0fb38f7dd98104745584d5015fa",
   "date": "2026-05-02T15:31:07Z",
   "message": "Revert \"make SaltedOutpointHasher noexcept\"\n\nThis reverts commit 67d99900b0d770038c9c5708553143137b124a6c."
  },
  {
   "sha": "16e77fdf1324e7419eff5adb358f2c9b27db948a",
   "date": "2026-05-02T15:31:08Z",
   "message": "util: allow caching outpoint hash codes\n\n`SaltedOutpointHasher` has been `noexcept` since #16957, which makes libstdc++ omit cached hash codes from `std::unordered_map` nodes.\n\nThat saves one `size_t` per node, but it also makes `CCoinsMap` recompute the outpoint hash in table operations that could otherwise reuse the cached code.\n\nDeclare the operator `noexcept(false)` to restore libstdc++ hash-code caching for this fast hash functor.\nThe implementation still does not throw; only the exception specification used by the container policy changes.\n\nThis restores a value that #16957 deliberately removed, but the pool-backed `CCoinsMap` allocation budget still accounts for implementations storing hash values.\nThe existing `PoolAllocator` comment says that \"in some cases the hash value is stored as well\" and that \"sizeof(void*)*4\" overhead \"should thus be sufficient so that all implementations can allocate the nodes from the PoolAllocator.\"\n\nAdd a unit test for the exception-specification contract and the libstdc++ fast-hash classification."
  }
 ],
 "timeline": [
  {
   "t": "2026-05-28T09:43:19Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/util/hasher.h",
   "commit": "fb0c20756768c0fb38f7dd98104745584d5015fa",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: In this particular case, I don't see a value in separating into 2 commits, I would prefer a single commit."
  },
  {
   "t": "2026-05-28T09:46:08Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/hash_tests.cpp",
   "commit": "16e77fdf1324e7419eff5adb358f2c9b27db948a",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: I suggest removing the blank lines here. A blank line as a separator between project and external include makes sense, but these are not needed."
  },
  {
   "t": "2026-05-28T09:46:52Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/hash_tests.cpp",
   "commit": "16e77fdf1324e7419eff5adb358f2c9b27db948a",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: I suggest adding a blank line here, to show that the two parts are separate checks."
  },
  {
   "t": "2026-05-28T09:54:14Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/util/hasher.h",
   "commit": "16e77fdf1324e7419eff5adb358f2c9b27db948a",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nHave you considered controlling hash-caching via `__is_fast_hash` (as described in the mentioned link)? Seems more clear than abusing 'noexcept'. Or that would not work on other platforms?"
  },
  {
   "t": "2026-05-28T10:02:14Z",
   "kind": "review",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "16e77fdf1324e7419eff5adb358f2c9b27db948a",
   "text": "Concept ACK 16e77fdf1324e7419eff5adb358f2c9b27db948a\n\nThis PR attempts to improve performance, by allowing `CCoinsMap` key hashes to be cached.\nIn general, the tradeoff between extra memory and extra CPU is not clear, but in this case benchmarks indicate that caching (more memory, less CPU) results in faster execution, therefore it should be applied.\nCode-wise I don't see issues or dangers reverting the `noexcept` on the hasher to `noexcept(false)`. Left some nits."
  }
 ],
 "labels_log": [
  {
   "t": "2026-05-02T16:22:28Z",
   "action": "labeled",
   "label": "UTXO Db and Indexes",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-21T23:44:36Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-05-05T15:36:58Z",
   "kind": "convert_to_draft",
   "who": "l0rinc"
  },
  {
   "t": "2026-05-05T15:36:58Z",
   "kind": "convert_to_draft",
   "who": "l0rinc"
  }
 ],
 "text_chars": 13602,
 "text_tokens_estimate": 3400,
 "changed_paths": [
  "src/test/hash_tests.cpp",
  "src/util/hasher.h"
 ],
 "files": [
  {
   "path": "src/test/hash_tests.cpp",
   "add": 17,
   "del": 0
  },
  {
   "path": "src/util/hasher.h",
   "add": 4,
   "del": 7
  }
 ],
 "test_lines": 17,
 "git": {
  "head": "16e77fdf1324e7419eff5adb358f2c9b27db948a",
  "head_matches_backup": true,
  "base": "8f4a3ba8972dae9412ba975a040cea22c227f983",
  "commits": [
   {
    "sha": "fb0c207567",
    "subject": "Revert \"make SaltedOutpointHasher noexcept\"",
    "files": 1,
    "add": 1,
    "del": 10
   },
   {
    "sha": "16e77fdf13",
    "subject": "util: allow caching outpoint hash codes",
    "files": 2,
    "add": 24,
    "del": 1
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "531a342ef1b8e036",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}