This is a WIP to improve the MallocUsage function, with logic to measure actual usage in the unit test code.
WIP: improve MallocUsage and test in CI #36549
pull sipa wants to merge 3 commits into bitcoin:master from sipa:202610_mallocusage changing 4 files +356 −12-
sipa commented at 4:27 AM on October 16, 2026: member
-
DrahtBot commented at 4:27 AM on October 16, 2026: contributor
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--006a51241073e994b41acfe9ec718e94-->
External sites
- Code coverage & benchmarks: https://corecheck.dev/bitcoin/bitcoin/pulls/36549
- Ralph review <img src=https://review.fish.foo/assets/bitcoin/bitcoin/36549.png alt="Ralph review status"> https://git.fish.foo/bitcoin/bitcoin/pulls/36549#author-ralph
<!--021abf342d371248e50ceaed478a90ca-->
Reviews
See the guideline and AI policy for information on the review process. A summary of reviews will appear here.
<!--5faf32d7da4f0f540f40219e4f7537a3-->
- sipa force-pushed on Oct 16, 2026
- DrahtBot added the label CI failed on Oct 16, 2026
-
DrahtBot commented at 4:39 AM on October 16, 2026: contributor
<!--85328a0da195eb286784d51f73fa0af9-->
🚧 At least one of the CI tasks failed. <sub>Task
macOS-cross to arm64: https://github.com/bitcoin/bitcoin/actions/runs/38850721587/job/116811104368</sub> <sub>LLM reason (✨ experimental): CI failed during compilation becauseRandomSizesinmemusage_tests.cppis unused, and Clang treats that warning as an error.</sub><details><summary>Hints</summary>
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.
</details>
- sipa force-pushed on Oct 16, 2026
- sipa force-pushed on Oct 16, 2026
-
8b5b4927f4
memusage: model real malloc implementations in MallocUsage
The previous formula was measured on glibc 2.19 and overestimates the size of glibc chunks by 16 bytes for half of all request sizes, ignores glibc's minimum chunk size, and has no notion of mmapped allocations. Model the allocators actually used by Linux, macOS and Windows builds: - glibc (ptmalloc2, all Linux release builds): chunks of max(MINSIZE, round_up(size + sizeof(size_t), MALLOC_ALIGNMENT)) bytes, with chunks of 128 KiB or more mmapped as whole pages. This matches glibc 2.43 exactly on x86_64, i386, aarch64, armhf, armel, riscv64, ppc64, ppc64le, ppc, s390x, loongarch64, alpha, hppa, m68k, sh4 and sparc64 (over 300000 request sizes each). - musl (mallocng, used by the Alpine CI job): a table of the steady-state cost per slot for each of its 48 size classes, including group headers and out-of-band metadata, which matches RSS measurements of musl 1.2.5 within 0.2%. - macOS (libmalloc): no in-band headers, with requests rounded up to 16 bytes up to 1008 bytes (tiny/nano), to 512 bytes up to 32 KiB (small), to 32 KiB up to 8 MiB (medium), and to whole pages beyond. This matches the CI measurements on macOS 15 (arm64) for every size the previous estimate got wrong (1-112, 1025-7200 and 32769-493424); the other sizes up to 4 MiB were within the test's tolerance. The 8 MiB limit of the medium range is libmalloc's default (from its source), not measured. - Windows x64 (the Windows heap, used by the UCRT's malloc): blocks of round_up(size + 8, 16) bytes, except that sizes up to 16 KiB come from its low-fragmentation heap, in buckets of 16 to 512 bytes granularity. This matches the CI measurements on Windows Server 2022 up to 512 KiB (beyond that, addresses no longer reflect usage). Blocks of over 0xFF000 bytes get their own VirtualAlloc() region, and are counted as their committed pages (not measured). Other platforms (BSDs) keep the previous estimate until they are measured. Also count the hash that libc++ stores in every std::unordered_map and std::unordered_set node, and the second list pointer of MSVC's STL. The previous MallocUsage() overestimated small allocations enough to hide this, but with the accurate models, pool_tests/memusage_test fails with libc++ (it finds that the pool allocator uses more memory than std::allocator, as the std_map nodes are counted as 24 instead of 32 bytes), and would with MSVC. That test also assumes that malloc has a per-allocation overhead, which the pool allocator avoids. macOS's allocator has none for small blocks, so there the pool really uses slightly more memory (for the list of its chunks). Only check that the pool saves memory where malloc's overhead for the nodes is significant.
-
04f59b28a0
test: compare MallocUsage against measured allocator behaviour
Add a unit test that measures how much memory malloc() actually consumes and fails if MallocUsage() is more than a factor 1+1/16 below, or more than a factor 1+1/8 above, the measured usage. The upper bound is looser because MallocUsage() may include out-of-band allocator metadata that the measurement does not see (up to 1/8 for musl's smallest size class). By default, a random set of sizes up to 4 MiB (512 KiB on Windows, see below) is tested: for every power of two 2^b, 16 >> (b / 4) (but at least one) sizes in [2^b, 2^(b+1)), so small sizes are tested more densely than large ones. Every mismatching size is reported on its own line, with its measured usage. When the ALL_SIZES constant is set to true, the test instead determines the usage of every size up to 4 MiB (searching for the boundaries between size classes, assuming usage does not decrease with size), reports mismatching size ranges, and prints the results in a form meant for writing MallocUsage() code, e.g. for glibc on x86_64: 1-24: 32 25-131048: round_up(size + 8, 16) 131049-4194280: round_up(size + 24, 4096) This takes about 25 seconds with glibc, and 5 with musl. In this mode, glibc's mmap threshold is pinned to its default, as glibc otherwise raises it whenever an mmapped block is freed, which would make the results depend on the order of the measurements. The usage of a size is measured as the smallest distance between neighbouring blocks (in address order), among up to 16384 live allocations of that size, that accounts for at least 10% of all pairs. This is robust against a fragmented heap (the test binary's heap holds over a thousand regularly spaced free holes at startup, which dominate smaller samples) and against allocators that place blocks at varying offsets within their slots (musl). Large blocks are often given page-granular memory, with the block at an offset within it that varies from block to block (musl cycles it), which makes neighbouring blocks slightly closer than the memory they use. Such an allocator returns a different address, less than a page away, when a block is freed and allocated again. If that happens (for distances of a page or more), the test also looks at the longest run of neighbours whose distances are within a page of that distance. If all of them lie within less than a page of a grid whose spacing is a multiple of the page size, that spacing is the usage. Blocks that are really spaced by something else (e.g. glibc's 4080-byte chunks) drift away from any such grid, though not necessarily within a short run (such as within one Windows heap segment), which is why this is only done when the offsets are seen to vary. Without this, musl's output in the ALL_SIZES mode has two lines per page above its mmap threshold, one of them 16 bytes too low; with it, the output is: 1-12: 16 13-156: round_up(size + 4, 16) 157-188: 192 ... (one line per size class) ... 104829-131036: 131072 131037-131051: 131056 131052-131052: 131072 131053-4194284: round_up(size + 20, 4096) On Windows, only sizes up to 512 KiB are tested: larger blocks often get their own heap segment or VirtualAlloc() region, so the distances between them reflect reserved address space rather than usage. Sanitizer builds are skipped, as MallocUsage() does not model their allocators.
-
60524a8d93
test: measure every allocation size in memusage_tests
Enable ALL_SIZES, so that the malloc_usage test measures the usage of every allocation size up to 4 MiB (instead of a random sample) and prints the results in a form meant for writing MallocUsage() code. This takes about 25 seconds with glibc, and 5 with musl.
- sipa force-pushed on Oct 16, 2026