benchmarking
Skill1.5k starsChanged 23 days agoMITView on GitHub
Picks the harness for a performance or memory question about ankerl::unordered_dense and gives the measurement rules, calibration constants and the list of ideas already measured and closed. Use before running any benchmark, A/B comparison, perf profile or size sweep, before quoting or comparing a speed or memory number, before editing scripts/ab/ or test/bench/, and before proposing or answering "would it make sense to" about an optimization of the index, hash, probe, layout, inlining, allocator or pipelines.
11 other projects use a version of this.
Trending
More talk lately.about the whole repository
Most recently taken up by
- gilsmt/skillstheir own version, first seen Oct 29, 2026
- JasonLovesDoggo/skillstheir own version, first seen Oct 25, 2026
- Signet-AI/signetaitheir own version, first seen Oct 13, 2026
- wellwelwel/pokutheir own version, first seen Oct 8, 2026
- nanvix/nanvixtheir own version, first seen Oct 8, 2026
- nubjs/nubtheir own version, first seen Oct 8, 2026
Who's talking about martinus/unordered_dense
- @sunitram.bsky.social“Updated my profile with a few links: Some C++ stuff I did: * nanobench nanobench.ankerl.com * …”Bluesky · Nov 27, 2024 · 3 likes
- A Hacker News story“A Fast, Densely Stored Hashmap Based On Robin-Hood Backward Shift Deletion”Hacker News · Mar 25, 2025 · 2 points
- @jeremy.laumon.name“Robin hood hashmap but dense, essentially I made my own version of github.com/martinus/uno... 😄”Bluesky · Mar 10, 2025 · 2 likes
- @jeremy.laumon.name“A dense open addressing hash map, very inspired from github.com/martinus/uno... Two cool tricks: - …”Bluesky · Nov 25, 2024 · 2 likes
- diasurgical/DevilutionX“Switch to a better hash map implementation”GitHub · Aug 4, 2024 · 2 reactions
The biggest 5 of 54 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 54
Talked about on GitHub, Bluesky and Hacker News (54 times).
On GitHub · 48
- Aug 4, 2024Switch to a better hash map implementation — diasurgical/DevilutionX2 reactions
- Sep 16, 2026unordered_dense: add version 5.0.1 — conan-io/conan-center-index1 reaction · 2 comments
- Feb 22, 2024build: Replace robin-hood-hashing with unordered_dense — KhronosGroup/Vulkan-ValidationLayers1 reaction · 52 comments
- Nov 6, 2026Update 4.9.2 — Esri/unordered_dense0 reactions · 1 comment
- Oct 22, 2026unordered_dense 5.3.3 — Homebrew/homebrew-core0 reactions · 1 comment
43 more on GitHub
- Oct 9, 2026unordered_dense 5.3.2 — Homebrew/homebrew-core0 reactions · 1 comment
- Oct 9, 2026Update `ankerl/unordered_dense` Meson subproject to 5.3.2 — versatica/mediasoup0 reactions
- Oct 4, 2026unordered_dense 5.3.1 — Homebrew/homebrew-core0 reactions · 1 comment
- Oct 3, 2026unordered_dense 5.3.0 — Homebrew/homebrew-core0 reactions · 1 comment
- Sep 27, 2026unordered_dense 5.2.0 — Homebrew/homebrew-core0 reactions · 1 comment
- Sep 26, 2026unordered_dense 5.1.0 — Homebrew/homebrew-core0 reactions · 1 comment
- Sep 16, 2026[unordered-dense] Update to 5.0.1 — microsoft/vcpkg0 reactions
- Sep 15, 2026unordered_dense: 4.11.0 -> 5.3.3 — NixOS/nixpkgs0 reactions · 5 comments
- Sep 14, 2026unordered_dense 5.0.1 — Homebrew/homebrew-core0 reactions · 1 comment
- Sep 14, 2026unordered_dense 5.0.0 — Homebrew/homebrew-core0 reactions · 1 comment
- Sep 4, 2026Write up and name the fingerprint-ordered robin hood field from unordered_dense — martinus/ai0 reactions
- Sep 4, 2026unordered_dense: 4.9.2 -> 4.11.0 — NixOS/nixpkgs0 reactions
- Sep 3, 2026seastar: allocation failure can terminate or leave chunked_hash_map state inconsistent — redpanda-data/redpanda0 reactions · 8 comments
- Sep 3, 2026unordered_dense 4.11.0 — Homebrew/homebrew-core0 reactions · 1 comment
- Sep 2, 2026unordered_dense 4.10.0 — Homebrew/homebrew-core0 reactions · 1 comment
- Aug 25, 2026unordered_dense: 4.9.1 -> 4.9.2 — NixOS/nixpkgs0 reactions
- Aug 16, 2026[unordered-dense] update to 4.9.2 — microsoft/vcpkg0 reactions
- Aug 16, 2026unordered_dense 4.9.2 — Homebrew/homebrew-core0 reactions · 1 comment
- Aug 16, 2026unordered_dense: 4.9.0 -> 4.9.1 — NixOS/nixpkgs0 reactions
- Aug 10, 2026unordered_dense 4.9.1 — Homebrew/homebrew-core0 reactions · 1 comment
- Aug 7, 2026unordered_dense: 4.8.1 -> 4.9.0 — NixOS/nixpkgs0 reactions
- Aug 7, 2026unordered_dense 4.9.0 — Homebrew/homebrew-core0 reactions · 1 comment
- Apr 29, 2026Feature proposal: IdSetSparse for Ids spanning a large value range — osmcode/libosmium0 reactions · 1 comment
- Nov 9, 2025unordered_dense: 4.8.0 -> 4.8.1 — NixOS/nixpkgs0 reactions · 1 comment
- Nov 2, 2025[unordered-dense] Update to 4.8.1 — microsoft/vcpkg0 reactions
- Oct 27, 2025unordered_dense: 4.7.0 -> 4.8.0 — NixOS/nixpkgs0 reactions · 3 comments
- Oct 27, 2025[unordered-dense] Update to 4.8.0 — microsoft/vcpkg0 reactions
- Oct 14, 2025unordered_dense: 4.6.0 -> 4.7.0 — NixOS/nixpkgs0 reactions · 2 comments
- Oct 8, 2025unordered_dense: 4.5.0 -> 4.6.0 — NixOS/nixpkgs0 reactions · 2 comments
- Jul 9, 2025Update unordered_dense.h — OrcaSlicer/OrcaSlicer0 reactions · 1 comment
- May 20, 2025Dedupe paths per-EvalNode — danvk/hybrid-boggle0 reactions · 2 comments
- Apr 8, 2025rsx: Replace robin_hood with ankerl::unordered_dense — RPCS3/rpcs30 reactions · 1 comment
- Feb 6, 2025[C++] (Possibly) Adopt unordered_map replacement — apache/arrow0 reactions · 1 comment
- Dec 11, 2024unordered_dense: add new version 4.5.0 — conan-io/conan-center-index0 reactions
- Dec 4, 2024[unordered-dense] update to 4.5.0 — microsoft/vcpkg0 reactions
- Jul 5, 2024clang++, libstdc++14, std=c++23 : lots of library code in mainstream libraries fails to build — llvm/llvm-project0 reactions · 12 comments
- Mar 15, 2024segment_map: Also segment the bucket array — redpanda-data/unordered_dense0 reactions · 2 comments
- Jan 13, 2024Use unordered_dense for hash tables — CelestiaProject/Celestia0 reactions · 3 comments
- Apr 20, 2023rebind makes build fail in boost::small_vector with libstdc++-13 and later — boostorg/container0 reactions · 1 comment
- Mar 21, 2023Change `robin-hood-hashing` to `unordered_dense` — ComputationalBiomechanicsLab/opensim-creator0 reactions
- Nov 21, 2022Improving indexing and mapping performance with low cost — iqbal-lab-org/pandora0 reactions · 3 comments
- Jul 21, 2022Add windows build — martinus/svector0 reactions · 1 comment
- Jul 19, 2022Add fuzzing, same way as done for https://github.com/martinus/unordered_dense — martinus/svector0 reactions · 1 comment
On Bluesky · 4
- Nov 27, 2024Updated my profile with a few links: Some C++ stuff I did: * nanobench nanobench.ankerl.com * unordered_dense github.com/martinus/uno... *… — @sunitram.bsky.social3 likes and reposts
- Mar 10, 2025Robin hood hashmap but dense, essentially I made my own version of github.com/martinus/uno... 😄 — @jeremy.laumon.name2 likes and reposts
- Nov 25, 2024A dense open addressing hash map, very inspired from github.com/martinus/uno... Two cool tricks: - key/values are stored separately from… — @jeremy.laumon.name2 likes and reposts · 2 replies
- Dec 11, 2024As noted, mine is about 10x slower with libc++'s std::unordered_map, so your approach might be faster? This is the hash table I'm using,… — @tristanbrindle.com0 likes and reposts · 1 reply
On Hacker News · 2
- Mar 25, 2025A Fast, Densely Stored Hashmap Based On Robin-Hood Backward Shift Deletion2 points
- Jul 11, 2023unordered_dense: A Fast, Densely Stored Hashmap Based On Backward Shift Deletion1 point
Other versions11 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 11
- Blaizzy/mlx-vlm★ 5.8ksame name
- nubjs/nub★ 4.4ksame name
- wellwelwel/poku★ 1.2ksame name
- nanvix/nanvix★ 318same name
- Signet-AI/signetai★ 313same name
- abinauv/business-consulting★ 28same name
- gilsmt/skills★ 2same name
- glennawatson/CSharpAgentSkills★ 2same name
- h-mascot/Enterprise-Crew-skills★ 1same name
- CAPP-Financials/Consulting_Brain★ 0same name
1 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
The file
We split this skill into the rules it gives an agent
2 rules. 0 are common, 0 are distinctive, and 2 are very specific to this project.
0
Common
Most of the 11 edited versions we read keep these.
0
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 26
No builds or other heavy work while a benchmark runs.
- Very specific: line 29
Long runs: one background call, no polling: Start it with run_in_background, tee its output to /home/martinus/gra/<name>/*.txt (the task's …
“Common” is measured: kept by at least 6 in 10 of the 11 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Benchmarking unordered_dense
Evidence for every rule: notes/index-design.md; grep the quoted phrase. Before proposing an
optimization, read already-measured.md: an idea listed there is closed
unless you bring new evidence.
The score
The metric: bench_quick_overall_udm, fifteen workloads (iterate-while-modifying, insert/erase,
build, churn at a fixed size, find at 50% hits) × (map<uint64_t,size_t>, map<string,size_t>,
map<uint64_t,big_value>), geometric mean, lower is better. Every workload's table is ≤ 200000
entries; churn is 50000. Anything past L2/L3 is invisible to it and needs the size-axis harnesses.
./builddir/clang_release/test/udm-test -ns -tc=bench_quick_overall_udm # the score (-ns: benchmarks are skip()ed)
./builddir/clang_release/test/udm-test -ns -ts=bench # every benchmarkRunning a measurement
- No builds or other heavy work while a benchmark runs. Pin with
AB_CORE=2. AB_BUILDon/home/martinus/gra/<name>, never/tmp(tmpfs; a full/tmpwedges every shell command). Delete the directory once the numbers are in the notes.- Long runs: one background call, no polling. Start it with
run_in_background,teeits output to/home/martinus/gra/<name>/*.txt(the task's own output file is on/tmp), and do independent work until the notification. The command cap is 1 hour: split sweeps at ~45 min. - A long run's variants are fixed when it compiles, so make them patches or template parameters,
never "the working tree": on 2026-10-02 a trigger edited after a run had built its binaries
meant a 25-minute restart.
scripts/ab/drift_variants.shapplies patches to a copy. - A timed region under a few milliseconds is noise:
drift_variants.sh's firstmiss/hitcells timed 5236 lookups (40 us) and read 0.96-1.08 for a layout-only change; 3M lookups made them consistent across sizes. Size the reps from time, not from the table. - Smoke every harness at the smallest size, one round, before the real run. Three scripts in
October 2026 failed only at run time:
env VAR=x fncannot call a shell function;ls_l1_d_tlb_miss.all_l2_dtlb_missis not an event;$!aftercmd | tee &istee's pid. - A variant of code under development is a template parameter or an API, not a patch:
index_load_nocheck.patchwent stale three times in one afternoon (#299) and becametrust::unchecked. Patches are for variants of code that is not changing. - A timed function that creates a map hands it out of the clock to be destroyed, and RSS is measured before the timed rounds: #299's string loads read 14 ns/entry of frees as load time, and an RSS after the rounds read 0.2 B/entry for a 27 B/entry map.
- Verify the mechanism is engaged before A/B-ing it. A setting that did not take effect measures
as a clean null. Huge pages:
AnonHugePagesin/proc/<pid>/smaps_rollupmid-run, orperf stat -e ls_l1_d_tlb_miss.tlb_reload_2m_l2_hit. Prefetch/inlining: the symbol table (nm -C | grep -c) andperf recordof the hot function. - perf events that exist on this machine (Zen 4, perf 6.x):
cycles,instructions,branch-misses,dTLB-load-misses,ls_l1_d_tlb_miss.{all,all_l2_miss,tlb_reload_4k_l2_hit,tlb_reload_2m_l2_hit,tlb_reload_coalesced_page_hit}. Four or more events multiplex; three per run for clean counts.cpuidis not installed.
Which measurement answers which question
| question | tool | reads |
| does a header change move the score, ≥ ~10%, inlining untouched | scripts/ab/run.sh [-c g++] [-r REV] all 12 (paired, one process, octave geomean) | believe the ratio when the CI excludes 100% |
anything touching always_inline, a function's size, a template boundary, or < 10% | AB_BUILD=/home/martinus/gra/x scripts/ab/solo.sh [-c g++] [-r main] 5 (one header per binary, alternated) then scripts/ab/perwl.sh /home/martinus/gra/x | solo.sh prints baseline/candidate (>1 = candidate faster; run.sh reads the same way). perwl.sh prints candidate/baseline ins/op per workload: deterministic to 4 decimals, the decisive number |
| the mechanism (cycles, misses, TLB) | perf stat / perf record on the score binary or scripts/ab/maps_one.sh (one map per binary) | perf record first: an out-of-line symbol in nm is not evidence the hot path calls it (#268) |
| an environment setting: page size, allocator tunable, governor | scripts/ab/same_binary.sh 5 GLIBC_TUNABLES=glibc.malloc.hugetlb=1 | same binary, no layout band; the paired harness cannot see this (both sides share a process) |
| the size axis, other maps, allocators | scripts/ab/huge_pages.sh, scripts/ab/maps.sh, scripts/ab/prefetch_lines.sh, ... (all self-contained, variant = template instantiation, interleaved rounds, medians) | never one size: sweep, and sweep across the cache boundary |
| a ratio against a map that is not this header (boost, another slots-per-group) | scripts/ab/run.sh -b -p 50 (2.4x the default's wall clock) | five points are worth up to 26% there and 0.3-1.1% (2.4% on rhitstr) against another revision of this header: the two sawtooths are only in phase in the second case (#274) |
| how this map stands against every map a caller might pick, in their own default configurations | AB_CORE=2 AB_BUILD=/home/martinus/gra/x scripts/ab/bench_readme.sh [-u v5.2.0] (2h45m, 25 maps x 2 key types x 5 workloads; -u builds this map's rows from a release; -w memory re-takes one panel) | writes doc/bench_readme.csv and the README's four SVGs (this map and 4.11.0 against the other libraries, and the opt-in shapes on their own). Every map gets its own hash here, which maps.sh deliberately does not do (#277) |
| which parts of the insert to inline and which to call | scripts/ab/shape_search/run.sh (gen, score x2, udb3, ch x2, corpus x2, rank; ~3 h, one stage per call) | the rank line: smallest worst ratio per compiler, fixed rule; the Pareto set shows what the alternatives trade |
| any change to what an insert or lookup inlines into its caller, or to the shape of the insert | AB_CORE=2 AB_BUILD=/home/martinus/gra/x scripts/ab/caller_corpus.sh main HEAD (9 caller loops x 2 sizes x 2 compilers, one binary each) | the worst ratio line per compiler, not the geomean: 5.2.0 won the score and read 3.3x (clang) and 3.6x (gcc) behind the best shape in one loop here. The loops that pick keys with % and store into the result can fall into the spill trap ("stored and reloaded") depending on how much is inlined; a change must not add a cliff |
| is the paired harness biased today | scripts/ab/run.sh -r HEAD (same header both sides) | one build is one sample of the layout band: rmissstr 1.036 then 0.993-1.008, rhit64 0.976-1.000, hashstr 0.872-1.05. Re-take it per build and retire anything under ~5% |
Calibration constants, all measured: run-to-run drift 1–2%; code layout luck ±3% (two runs of the
same two binaries are one layout sample; -falign-functions=32 re-rolls it: #262 read 0.9975 and
1.0052 on two layouts); hashstr control swings 0.87–1.13 in the paired harness (its floor); a
THP-always runner scores 2.6–3.6% above a madvise one for no reason in the code — record
/sys/kernel/mm/transparent_hugepage/enabled before comparing machines; nanobench err% > 3 = rerun.
Measurement rules (each cost a wrong answer first; grep the phrase)
- Release builds only. Baseline first, then 2–3 comparisons. Never compare runs from different times -- including a fresh sweep against a stored CSV, which is a systematic few percent and invented a 2-3% big-value regression that
maps.sh -r REV(both headers, one process, interleaved) showed was 2.3% the other way.solo.sh -rcannot reach a header older than the test suite:fab7984will not build against today'shuge_page_allocator.h. grep "Comparing today's runs" - A lookup harness here measures throughput, not latency (
lookups()draws its key from an rng, so several are in flight). A dependent chain is 1.4x slower for this map at a million entries and 3.0x for boost, and the two rank the maps oppositely: boost wins the independent hit 1.69x and loses the chain 22.0 to 16.6. Label which one a number is.scripts/ab/latency.cppruns both; its independent loop must not pick keys by reading an array, or it measures that array's misses. grep "measure throughput" - A million entries is not past the cache on this machine: 64 MB of L3 in two 32 MB slices,
AB_COREpins to one, andmap<uint64_t, uint64_t>at 1M is 26 MB here and 32 MB in boost. Two million is the first size clearly out. grep "not past the cache" - Never quote a ratio from one table size. Load factor sawtooths ½→max between doublings and two
indexes double at different sizes: 11 slots read 1.384 at one size, 1.039 over the octave; churn64
reversed sign. Every boost ratio not labelled "octave geomean" is a point measurement. grep "octave".
Five points across that octave never land on its peak (load 0.763 of a maximum of 0.800) and
read about two thirds of the amplitude
-p 50reads (rmiss641.29x against 1.46x). Five is right for a header-against-header A/B and wrong for anything out of phase with this map. grep "Fifty points". - Sweep across the cache boundary for any prefetch/pipeline. Lookahead costs 13–17 instr/element
whether needed or not;
replace()shipped a fifth slower below 65k after being measured only above its crossover. Two sizes on one side of a cache are one size. grep "crossover". - The control for a pipeline is the same entry point with the ring removed, as a second binary
(
solo.sh), not the caller's loop of single calls (that carries the 15-instruction call boundary). - Instruction counts are immune to layout, not to inlining. A count that moved because the
inliner moved is real for the shipped binary and transfers to nothing: #254's 133→119.5 vanished
under
noinlineand never existed under gcc. Before generalising a count: pin withnoinline, check the second compiler, and readperwl.shfor workloads that cannot run the changed code — if they moved, it was the inliner (#268: onechurninstantiation grew 7065→10384 bytes). grep "inlining artifact". - A scratch one-map TU can hide a redundancy (#254) or invent one (#268: a
m_group_maskreload real in the scratch TU, hoisted in the score binary). Read the disassembly of the score binary's hot function, found viaperf record, not a scratch TU's. - Profile before disassembling. #268's only real find had a 0.2% ceiling readable from one
perf record; the audit spent a day reaching it. grep "sweep run over find and churn". - The paired harness gets the sign wrong on anything that changes inlining (both headers in one
TU exhaust the budget): removing an
always_inlineread 0.979 paired and 1.017 solo. grep "solo". - A benchmark that picks its variant inside the timed loop measures the dispatch (14.11 vs 13.43 ns for identical addresses). Variant = template parameter; a gated loop = two instantiations, never a branch (a predicted branch with a ring, a lambda and an array hanging off it cost 10%).
- A benchmark that rebuilds its subject every round measures the allocator, bimodally (4.40 / 2.85 / 4.76 ns for one binary). Warm the subject, report the median round.
- A benchmark with a small per-epoch batch must advance its own randomness; a replayed key sequence is learned by the predictor (2.7x flattery; made twice, two tools). grep "replay".
- A benchmark's input model can make a wrong heuristic look right (a fresh-key rate that is stationary in the harness and 64x wrong on real data). Unit tests judge correctness, benchmarks judge speed. grep "birthday".
- A pipeline's payoff depends on the caller's data, not only the size:
replace()'s ring is 1.5x with no duplicates and −16% with a quarter, same size. Pick gates on the geomean over key types and rates and state what they give up. grep "duplicate rate". - Walk value containers with iterators in any loop that also places — a
uint8_tfingerprint store may alias the container's data pointer, so an indexed read reloads it per element (10.43→2.74 ns). Hold one cursor fewer than feels natural (size(), notlast): a fourth live value cost 3.5% cycles with fewer instructions. grep "cursor". - The page size is worth more than anything left in the code: L1 dTLB = 72 entries = 288 KB;
every scored workload but
iterateexceeds it; 1.6 G L2-TLB lookups per score pass, 107x fewer on 2 MB pages. It is not the map's to set (process policy, source break on the allocator type, machine-dependent results): opt-in allocator #231 or the environment. grep "4 KB pages".
Rules the scored workloads obey (test/bench/workloads.h; break one and the score measures the input)
String keys 8–135 bytes skewed short (fixed length = predictable hash dispatch); integer keys through
a bijection (small sequential ints never collide); something must grow, something must churn at a
fixed size, something must carry a >8-byte value; tame_allocator() in every workload (else 38% of a
build is the page-fault handler); churn draws fresh keys (recycling halves the drift). Scores before
and after any workload change are not comparable, and the change is the fix, not a workaround.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

