Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
# helly25_mbo module or any BCR offering; the generated BCR source archive
# carries LFS pointer files here, which is harmless (nothing builds them).
# See mbo/hash/measurements/README.md.
mbo/hash/measurements/data/**/*.tgz filter=lfs diff=lfs merge=lfs -text
mbo/hash/measurements/data/*.tgz filter=lfs diff=lfs merge=lfs -text
7 changes: 6 additions & 1 deletion mbo/hash/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,10 @@ the libc++ SSO cap, 47/48 and 63/64 the short-chain steps). The tool's full mode
sweeps a denser exponential curve.

Full-sweep curves (log-log axes, `run_measurements.py`; the tables below are the
dense README subset):
dense README subset). Everything between the markers is regenerated per machine
by `hash_benchmark_report.py publish` from the committed data bundles:

<!-- BEGIN mbo/hash benchmark results (generated by `hash_benchmark_report.py publish`; DO NOT EDIT) -->

![mbo/hash 64-bit one-shot throughput vs key length, log-log](measurements/hash_throughput_64.svg)

Expand Down Expand Up @@ -290,6 +293,8 @@ actually pays (as opposed to the hot, size-predictable throughput loop above).
| 64 | 11.78 | 11.47 | 11.93 | **9.90** | 19.73 | 27.83 | 11.41 | 17.20 |
| 1024 | **26.46** | 26.74 | 37.26 | 65.11 | 69.52 | 229.4 | 604.2 | 97.80 |

<!-- END mbo/hash benchmark results -->

Reading the results: `rapidhash` leads small keys, but after the if-ladder load
path (see the design iterations) `mumbo` sits ~2.0 ns through 16 bytes -
within ~0.3 ns of rapidhash across the inline-`std::string` range (2.01 ns
Expand Down
8 changes: 8 additions & 0 deletions mbo/hash/hash_benchmark.cc
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ int main(int argc, char** argv) {
benchmark::AddCustomContext("compiler", "gcc-" + std::to_string(__GNUC__));
benchmark::AddCustomContext("compiler_version", __VERSION__);
#endif
// Emit the curated README size subset (kReadmeSizes) so the report tool extracts
// the small table straight from a FULL dataset - no separate fast run, and no
// second size list to drift (this C++ list is the single source of truth).
std::string readme_sizes;
for (const int size : mbo::hash::kReadmeSizes) {
readme_sizes += (readme_sizes.empty() ? "" : ",") + std::to_string(size);
}
benchmark::AddCustomContext("readme_sizes", readme_sizes);
benchmark::RunSpecifiedBenchmarks();
benchmark::Shutdown();
return 0;
Expand Down
101 changes: 48 additions & 53 deletions mbo/hash/measurements/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,12 @@ even invert the true ordering. So we:
mbo/hash/measurements/
README.md # this design doc
MODULE.bazel # separate dev module (isolates plotting deps)
run_measurements.py # one-shot authoritative runner (perf + chart + parallel smhasher)
hash_benchmark_report.py # run / store / tables / plot / smhasher (stdlib only)
run_measurements.py # per-machine runner: one full sweep + smhasher -> a data bundle (nothing else)
hash_benchmark_report.py # run / store / tables / plot / bundle / publish / verify (stdlib only)
build_smhasher3.sh # reproducible SMHasher3 build (clone + fixes + install plugin + container gcc)
smhasher3/mbohash.cpp # in-house mumbo/jumbo and dumbo SMHasher3 registration (includes the real headers)
hash_throughput_64.svg # published 64-bit throughput chart (committed; verifiable from data/)
hash_throughput_128.svg # published 128-bit throughput chart (committed; verifiable from data/)
data/<os-arch-cpu>/*.tgz # per-machine measurement bundles, Git LFS (see "Data storage")
charts/<slug>_<cc>_*.svg # published per-machine charts (committed; rendered by `publish`, verifiable)
data/<slug>_<cc>_*.tgz # per-machine measurement bundles, Git LFS, flat (see "Data storage")
```

The C++ benchmark itself (`mbo/hash/hash_benchmark.cc`,
Expand Down Expand Up @@ -101,76 +100,72 @@ distilled canonical JSON is ~38 KB / ~100 KB, plus the per-algorithm SMHasher3
logs. Across several machines that accumulates, so:

- **Per-machine bundle, Git LFS**
(`data/<os>-<arch>-<cpu-brand>/<slug>_<cores>c_<compiler>_<gitsha8>_<stamp>.tgz`): one
(`data/<slug>_<cores>c_<compiler>_<gitsha8>_<stamp>.tgz`): one
gzipped tarball per run holds the _whole_ dataset - the canonical
`results.json`, the raw `*_raw.json.gz` (for `compare.py` U-tests), and the
`smhasher.json` + per-algorithm logs. It is Git-LFS-tracked (`.gitattributes`),
so the blobs stay out of the main pack; unpack to inspect or A/B. Keep the
current bundle per machine (replace on re-measure); LFS history retains olds.
- **Published charts, plain git** (`hash_throughput_64.svg` / `_128.svg`): the
human-facing output embedded in the hash README. Small, and - the point -
_verifiable_: `hash_benchmark_report.py verify --bundle <tgz>` regenerates them
from the bundled canonical data and fails on any mismatch, so every published
figure is provably derived from a committed dataset and cannot be hand-edited
(`_svg_plot` is deterministic: geometry keyed only off the data, no wall-clock).
The README perf tables are likewise rendered from the canonical data (unpack a
bundle and run `tables`).
- **Published charts + tables, plain git** (`charts/<slug>_<compiler>_{64,128}.svg`
- the hash README perf section): rendered by `hash_benchmark_report.py publish`
from a SELECTED set of committed bundles - one labeled block per machine. Charts
carry the machine identifier (subtitle); the tables are the curated `kReadmeSizes`
subset extracted from the FULL dataset via the `readme_sizes` the benchmark emits
into its own context (no separate fast run, no second size list to drift). And -
the point - _verifiable_: `verify` re-renders from the same bundles (a manifest
publish leaves in the README) and diffs, so every figure is provably from
committed data and cannot be hand-edited (`_svg_plot` is deterministic). This is
the ONLY thing that writes to the tree; the measurement run writes nothing but
the bundle, so it stays authoritative and several runs can go back-to-back.
- The loose run outputs in `data/` are staging only and git-ignored; `bundle`
packs them into the `.tgz`. This directory is dev-only and `.bazelignore`'d, so
it never enters the module or BCR (the release archive carries LFS pointer
files here, which is harmless).

## Regenerate / contribute a machine

Run the one-shot runner on the machine you want to add, from the repo root and a
clean `main` checkout (so the provenance is authoritative). It runs the perf
sweep, renders + verifies the charts, runs the SMHasher3 battery, packs the
per-machine LFS bundle, and prints exactly what to commit:
Two decoupled steps: each machine produces a data bundle (that is ALL the run
writes to the tree, so it stays authoritative and runs can go back-to-back), then
a single `publish` renders the README from the machines you choose.

**1. Measure, per machine** (repo root, clean `main` for authoritative provenance):

```sh
# Everything, SMHasher3 batteries 4-at-a-time (perf runs first and alone):
mbo/hash/measurements/run_measurements.py --jobs 4
# Pick the toolchain (and thus the recorded compiler): --config clang uses the
# hermetic LLVM clang; omit --config for the native toolchain (gcc on Linux).
# Run once per compiler to compare them - the bundle filenames won't collide:
mbo/hash/measurements/run_measurements.py --config clang --jobs 4
# In-house family only, or perf/chart only:
mbo/hash/measurements/run_measurements.py --config clang --jobs 4 # clang; omit --config for native gcc
# faster: in-house family only, or perf only
mbo/hash/measurements/run_measurements.py --algos mumbo,jumbo,dumbo --jobs 1
mbo/hash/measurements/run_measurements.py --skip-smhasher
```

The performance sweep runs first and alone (SMHasher3 would contend for CPU and
skew the sub-ns numbers); the batteries are independent and their pass/fail is
load-independent, so `--jobs` runs several concurrently - trading cores for
wall-clock, not accuracy. SMHasher3 is built and run inside a container
(`build_smhasher3.sh`), so the runner invokes its Linux binary via `docker run`.
ONE full perf sweep runs first and alone (SMHasher3 would contend for CPU and
skew the sub-ns numbers), then the battery (`--jobs` at once; pass/fail is
load-independent, so that only trades cores for wall-clock). SMHasher3 is built
and run in a container (`build_smhasher3.sh`, via `docker run`). The run packs a
per-machine Git-LFS bundle and prints its path - commit it:

```sh
git add mbo/hash/measurements/data/<bundle>.tgz
git lfs push origin HEAD && git push
```

Or drive the steps individually (`bundle` packs a run; `verify` audits the
published charts against a bundle; unpack a bundle for its canonical JSON):
**2. Publish, once** - render the README from the bundles you want to feature:

```sh
# Perf sweep -> staged canonical results.json + raw.json.gz (from the repo root):
python3 mbo/hash/measurements/hash_benchmark_report.py run \
--reps 20 --warmup 0.05 \
--raw mbo/hash/measurements/data/raw.json.gz \
--out mbo/hash/measurements/data/results.json --tables

# Pack a run's staged outputs into the per-machine Git-LFS bundle:
python3 mbo/hash/measurements/hash_benchmark_report.py bundle \
--results mbo/hash/measurements/data/<stamp>_results.json \
--include mbo/hash/measurements/data/<stamp>_raw.json.gz \
mbo/hash/measurements/data/<stamp>_smhasher.json

# Verify the committed charts match a bundle's data (unpacks, re-renders, diffs):
python3 mbo/hash/measurements/hash_benchmark_report.py verify \
--bundle mbo/hash/measurements/data/<slug>/<bundle>.tgz

# Re-render the README tables / charts from a bundle's canonical JSON:
tar xzOf <bundle>.tgz results.json > /tmp/results.json
python3 mbo/hash/measurements/hash_benchmark_report.py tables --results /tmp/results.json
python3 mbo/hash/measurements/hash_benchmark_report.py plot --results /tmp/results.json --out /tmp/hash.svg
mbo/hash/measurements/hash_benchmark_report.py publish \
--bundles data/<m1>.tgz data/<m2>.tgz data/<m3>.tgz
git add mbo/hash/README.md mbo/hash/measurements/charts
```

`publish` cuts the marker region in the hash README and re-appends one labeled
block per bundle (charts + curated tables), in the listed order; `--bundles` is
the selection. `verify` (no args, from the repo root) re-checks every featured
bundle against the committed charts, reading the machine list back from the
README's manifest.

The pipeline steps are also usable individually: `run` (perf -> canonical JSON),
`bundle` (pack a run), `tables` / `plot` (render from any canonical JSON -
`tar xzOf <bundle>.tgz results.json` pulls one out), `publish`, `verify`.

## SMHasher3 quality (`smhasher`)

`hash_benchmark_report.py smhasher --algos all --smhasher3 <path>` runs the
Expand Down Expand Up @@ -200,7 +195,7 @@ Last verified run (2026-07): **mumbo-64/jumbo-128** and **dumbo-64** all PASS
Loose staging files the tool writes are prefixed `YYYYMMDD_HHMMSS_` (local wall
clock, one stamp per invocation) so runs never overwrite each other. The
committed artifact is the per-machine bundle
`data/<os>-<arch>-<cpu-brand>/<slug>_<cores>c_<compiler>_<gitsha8>_<stamp>.tgz` - the slug
`data/<slug>_<cores>c_<compiler>_<gitsha8>_<stamp>.tgz` - the slug
derived from the dataset's own `uname` + CPU brand, the `compiler` reported by the
benchmark binary (`clang-NN` / `gcc-NN`, so GCC and Clang builds on one machine
stay distinct), the SHA from its provenance - so a bundle is self-identifying and
Expand Down
5 changes: 2 additions & 3 deletions mbo/hash/measurements/data/.gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Loose run outputs (results.json, raw.json.gz, smhasher.json, logs) are staging
# only - they get packed into a per-machine LFS bundle `<slug>/<...>.tgz` by
# only - they get packed into a per-machine LFS bundle `<slug>_..._<stamp>.tgz` by
# `hash_benchmark_report.py bundle`. Commit the bundles (LFS-tracked, see
# ../../../.gitattributes), not the loose files. See ../README.md.
*
!.gitkeep
!.gitignore
!*/
!*/*.tgz
!*.tgz
Loading
Loading