From a7c36b06eb6286b9468472d6059b83fd3e311c24 Mon Sep 17 00:00:00 2001 From: helly25 <6420169+helly25@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:01:24 +0100 Subject: [PATCH 1/3] feat(hash): render latency + throughput (Short/Web) per width; add 128-bit throughput Report: distill and render the latency (ns, exact length) / throughput (GiB/s, bounded Short/Web distributions) split - each a chart + table per hash width, in order 64-latency, 64-throughput Short/Web, 128-latency, 128-throughput Short/Web, with absent sections dropped. Bundles are re-distilled from their raw on read, so old bundles still yield latency tables and no bundle needs re-packing when the tool changes; plot/compare/publish/verify all follow the new schema. Benchmark: add BmHash128Throughput and guard the 64-/128-bit paths by HasGetHash64 / HasGetHash128. Data: publish the merged-main (80a4c8a8) bundles (64-bit latency, 64-bit throughput Short/Web, 128-bit latency per machine; 128-bit throughput awaits a re-measure) and drop the superseded 1417af5e bundles + old throughput charts. Refreshed the README perf prose, "Reading the results", and SMHasher3 tables. --- CHANGELOG.md | 2 + mbo/hash/README.md | 425 ++++++++++-------- mbo/hash/hash_benchmark.cc | 63 ++- ...n-9-9950x-16-core-processor_gcc-15_128.svg | 40 -- ...en-9-9950x-16-core-processor_gcc-15_64.svg | 59 --- ...0x-16-core-processor_gcc-15_latency128.svg | 40 ++ ...50x-16-core-processor_gcc-15_latency64.svg | 59 +++ ...re-processor_gcc-15_throughput64_Short.svg | 42 ++ ...core-processor_gcc-15_throughput64_Web.svg | 47 ++ ...arm64-apple-m5-pro_clang-21_latency128.svg | 40 ++ ...-arm64-apple-m5-pro_clang-21_latency64.svg | 59 +++ ...ple-m5-pro_clang-21_throughput64_Short.svg | 40 ++ ...apple-m5-pro_clang-21_throughput64_Web.svg | 49 ++ .../macos-arm64-apple-m5-pro_clang-22_128.svg | 40 -- .../macos-arm64-apple-m5-pro_clang-22_64.svg | 59 --- ...or_10c_gcc-15_1417af5e_20260711_203603.tgz | 3 - ...or_10c_gcc-15_80a4c8a8_20260714_225122.tgz | 3 + ..._18c_clang-21_80a4c8a8_20260714_233133.tgz | 3 + ..._18c_clang-22_1417af5e_20260711_211526.tgz | 3 - .../measurements/hash_benchmark_report.py | 393 ++++++++++------ mbo/hash/measurements/hash_throughput_128.svg | 39 -- mbo/hash/measurements/hash_throughput_64.svg | 58 --- 22 files changed, 924 insertions(+), 642 deletions(-) delete mode 100644 mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_128.svg delete mode 100644 mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_64.svg create mode 100644 mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency128.svg create mode 100644 mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency64.svg create mode 100644 mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Short.svg create mode 100644 mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Web.svg create mode 100644 mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency128.svg create mode 100644 mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency64.svg create mode 100644 mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Short.svg create mode 100644 mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Web.svg delete mode 100644 mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-22_128.svg delete mode 100644 mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-22_64.svg delete mode 100644 mbo/hash/measurements/data/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_10c_gcc-15_1417af5e_20260711_203603.tgz create mode 100644 mbo/hash/measurements/data/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_10c_gcc-15_80a4c8a8_20260714_225122.tgz create mode 100644 mbo/hash/measurements/data/macos-arm64-apple-m5-pro_18c_clang-21_80a4c8a8_20260714_233133.tgz delete mode 100644 mbo/hash/measurements/data/macos-arm64-apple-m5-pro_18c_clang-22_1417af5e_20260711_211526.tgz delete mode 100644 mbo/hash/measurements/hash_throughput_128.svg delete mode 100644 mbo/hash/measurements/hash_throughput_64.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index f89421b..20c5006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # 0.13.2 +- Added `BmHash128Throughput` (128-bit bounded-range throughput) and guarded the 64-/128-bit benchmark paths by `HasGetHash64`/`HasGetHash128`. +- Reworked the measurement report around the latency (ns at exact length) / throughput (GiB/s over the Short/Web bounded distributions) split: each renders as chart + table per hash width, absent sections dropped; the report re-distills a bundle from its raw on read, so old bundles still yield latency tables and bundles need no re-packing when the tool changes. - Reworked the hash mixed-length benchmark: dropped the hash-indexed key walk that collapsed into a rho-cycle (near-zero anomalies) and added `BmHash64Throughput` reporting bytes/s over two documented length distributions (Short ≤128 B, Web ≤4096 B) truncated to each upper bound; the per-exact-length `BmHash64`/`BmHash128` remain the latency view. Distributions are exported as the `throughput_dists` context. - Added a `compare` command reporting per-case Δ% and a geomean between two datasets. - Made `tables`/`plot`/`compare`/`quality` accept a bundle `.tgz` or a results JSON, positionally or via `--results`/`--bundle`. diff --git a/mbo/hash/README.md b/mbo/hash/README.md index 982469b..b8f17a8 100644 --- a/mbo/hash/README.md +++ b/mbo/hash/README.md @@ -272,186 +272,245 @@ The two frameworks compose rather than compete - pick by contract: Measured with the tooling in [`mbo/hash/measurements/`](measurements/README.md) (`-c opt`). Each block below is one machine and compiler, generated from a -committed data bundle. Numbers are the mean of the 3 fastest of 9 runs (with -interleaving and warmup): on a shared machine the fastest runs are the least -contended, and averaging a few is steadier than the median at these -sub-nanosecond sizes. Bold marks the fastest per row; the tables use a curated -set of lengths (straddling the dispatch-tier and SSO boundaries), the log-log -charts a denser one. - -The **latency** benchmark models how a hash table actually calls a hash: a -stream of differently-sized keys whose per-length size dispatch cannot be -branch-predicted. Keys are drawn from two fixed, reproducible length -distributions - **Short-Identifier** (log-normal: identifiers, DB keys, UUIDs) -and **Web-URL** (heavy-tailed: paths and URLs) - sampled once with a fixed seed -and shared byte-for-byte across all algorithms (only the lengths matter; the -bytes are filler). Each distribution is an inverse-CDF (Cumulative Distribution -Function, see [Wikipedia](https://en.wikipedia.org/wiki/Cumulative_distribution_function)) -table capped by a 100% limit anchor `Lmax`: **128 B** for Short-Identifier (two L1 cache lines - the -AVX-512 / medium-key-to-bulk transition and a jemalloc/tcmalloc size-class -ceiling) and **4096 B** for Web-URL (one x86/ARM64 virtual page, where a larger -allocation can page-fault). Because the percentile draw is half-open `[0, 1)` it -never samples the `1.0` entry by chance (a 1024-key set misses the top region -~36% of the time), so exactly one key per set is pinned to `Lmax` - a guaranteed -worst-case anchor that keeps the curve bounded and exercises the SSO-spill and -bulk-tier code paths, while the other 1023 keys preserve the branch-prediction -noise. +committed data bundle. Numbers are the mean of the 3 best of 9 runs (with +interleaving and warmup): on a shared machine the least-contended runs are the +truest, and averaging a few is steadier than a single extreme. Bold marks the +winner per row. Tables use a curated set of lengths (straddling the dispatch-tier +and SSO boundaries); the log-log charts a denser one. + +Two views per hash width: + +- **Latency** (ns/hash, lower is better): the cost of hashing one key of an + _exact_ length in a hot loop - the per-length curve, for spotting the + size-dispatch and SSO cliffs at specific lengths. +- **Throughput** (GiB/s, higher is better): how fast a realistic, upper-**bounded** + mix of key lengths is processed. Keys are drawn from two fixed, reproducible + length distributions - **Short** (log-normal: identifiers, DB keys, UUIDs) and + **Web** (heavy-tailed: paths and URLs) - given as inverse-CDF (Cumulative + Distribution Function, see + [Wikipedia](https://en.wikipedia.org/wiki/Cumulative_distribution_function)) + tables, sampled once with a fixed seed and shared byte-for-byte across all + algorithms (only the lengths matter; the bytes are filler). Each distribution's + control-point lengths double as upper **bounds**: the mix is run truncated to + each bound (the kept buckets renormalized to 100%), giving a + `(max length -> GiB/s)` curve. The ceilings are `Lmax` = **128 B** for Short + (two L1 cache lines - the AVX-512 / medium-key-to-bulk transition and a + jemalloc/tcmalloc size-class ceiling) and **4096 B** for Web (one x86/ARM64 + virtual page). One key per bound is pinned to the bound length so the boundary + is always represented; the unpredictable length order defeats the size-dispatch + branch predictor, so the cost is measured, not modelled. Everything between the markers is generated per machine by `publish` from the committed bundles - regenerate it, don't hand-edit: - - -### Apple M5 Pro · arm64 · 18-core · clang-22 · 1417af5e - -![mbo/hash 64-bit throughput vs key length, log-log](measurements/charts/macos-arm64-apple-m5-pro_clang-22_64.svg) -![mbo/hash 128-bit throughput vs key length, log-log](measurements/charts/macos-arm64-apple-m5-pro_clang-22_128.svg) - - - -#### 64-bit one-shot throughput (ns/op, mean of the 3 fastest of 9 reps; lower is better) - -| Length | mumbo | rapidhash | xxh3 | xxh64 | murmur3 | siphash24 | fnv1a | dumbo | -| -----: | --------- | --------- | ----- | ----- | ------- | --------- | -------- | -------- | -| 1B | 2.17 | 1.97 | 2.43 | 2.34 | 2.80 | 6.43 | **0.49** | 1.57 | -| 3B | 2.30 | 1.96 | 2.42 | 3.28 | 3.00 | 6.60 | **1.03** | 2.09 | -| 5B | 2.34 | 1.74 | 2.46 | 2.41 | 3.02 | 6.82 | 1.80 | **1.48** | -| 7B | 2.32 | 1.74 | 2.50 | 3.56 | 3.03 | 6.90 | 2.54 | **1.48** | -| 8B | 2.09 | 1.68 | 2.42 | 2.69 | 3.04 | 9.13 | 2.99 | **1.47** | -| 11B | 2.03 | **1.69** | 2.39 | 3.46 | 3.54 | 9.27 | 4.07 | 2.44 | -| 15B | 2.00 | **1.70** | 2.36 | 3.85 | 3.62 | 9.26 | 6.14 | 1.89 | -| 16B | 2.01 | **1.71** | 2.31 | 3.17 | 3.87 | 12.08 | 6.46 | 1.90 | -| 19B | 2.44 | **1.98** | 2.89 | 4.24 | 4.46 | 12.26 | 7.59 | 3.00 | -| 22B | 2.44 | **1.94** | 2.89 | 3.95 | 4.61 | 12.26 | 8.99 | 2.48 | -| 27B | 2.44 | **1.95** | 2.89 | 5.09 | 5.10 | 15.27 | 11.69 | 3.54 | -| 32B | 2.44 | **1.99** | 2.89 | 5.07 | 5.18 | 18.43 | 15.39 | 3.00 | -| 38B | 2.88 | **2.34** | 4.23 | 6.82 | 5.84 | 18.41 | 19.35 | 3.62 | -| 47B | 2.87 | **2.35** | 4.22 | 8.52 | 6.60 | 21.79 | 27.44 | 4.26 | -| 48B | 2.88 | **2.34** | 4.23 | 6.72 | 6.72 | 25.24 | 28.93 | 4.22 | -| 63B | 3.37 | **2.84** | 4.22 | 10.31 | 8.14 | 28.67 | 40.26 | 5.49 | -| 64B | 3.37 | **2.89** | 4.23 | 6.68 | 8.40 | 31.95 | 41.07 | 5.49 | -| 127B | 6.04 | **4.85** | 6.92 | 13.70 | 15.14 | 57.83 | 117.3 | 12.41 | -| 128B | 6.55 | **4.84** | 6.91 | 9.66 | 15.55 | 61.64 | 118.4 | 12.38 | -| 256B | 8.69 | **7.44** | 26.46 | 16.12 | 32.96 | 119.5 | 287.7 | 31.00 | -| 1Ki | 23.73 | **22.50** | 58.92 | 55.30 | 161.1 | 479.9 | 1271 | 193.5 | -| 4Ki | **82.37** | 83.87 | 176.3 | 222.9 | 692.6 | 1915 | 5503 | 871.8 | - -#### 128-bit one-shot throughput (ns/op, mean of the 3 fastest of 9 reps; native-128 algorithms only) - -| Length | jumbo | xxh3 | murmur3 | -| -----: | --------- | -------- | -------- | -| 1B | 3.12 | 2.88 | **2.81** | -| 3B | 3.23 | **2.92** | 2.99 | -| 5B | 3.08 | **2.47** | 3.02 | -| 7B | 3.07 | **2.48** | 3.04 | -| 8B | 2.96 | **2.50** | 3.01 | -| 11B | 2.96 | **2.96** | 3.54 | -| 15B | **2.96** | 2.96 | 3.64 | -| 16B | **2.96** | 2.96 | 3.86 | -| 19B | **3.86** | 4.05 | 4.47 | -| 22B | **3.86** | 3.96 | 4.57 | -| 27B | **3.86** | 3.94 | 5.11 | -| 32B | **3.86** | 3.98 | 5.23 | -| 38B | **4.45** | 5.30 | 5.86 | -| 47B | **4.44** | 5.31 | 6.68 | -| 48B | **4.43** | 5.31 | 6.81 | -| 63B | **5.29** | 5.31 | 8.17 | -| 64B | 6.21 | **5.31** | 8.55 | -| 127B | 8.77 | **8.33** | 15.27 | -| 128B | **7.48** | 8.32 | 15.61 | -| 256B | **10.55** | 28.10 | 32.83 | -| 1Ki | **31.69** | 60.54 | 162.2 | -| 4Ki | **120.4** | 177.7 | 700.6 | - -#### Mixed-length latency (ns/hash, mean of the 3 fastest of 9 reps; lower is better) - -| max len | mumbo | rapidhash | xxh3 | xxh64 | murmur3 | siphash24 | fnv1a | dumbo | -| ------: | --------- | --------- | ----- | ----- | ------- | --------- | ----- | ----- | -| 16 | **9.88** | 10.68 | 11.52 | 13.98 | 15.48 | 18.99 | 14.70 | 12.67 | -| 64 | **12.37** | 12.64 | 12.74 | 20.78 | 20.82 | 29.22 | 35.14 | 18.82 | -| 1024 | 27.81 | **27.55** | 37.07 | 66.34 | 69.60 | 231.0 | 604.0 | 99.15 | - -### AMD Ryzen 9 9950X 16-Core Processor · x86_64 · 10-core · gcc-15 · 1417af5e - -![mbo/hash 64-bit throughput vs key length, log-log](measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_64.svg) -![mbo/hash 128-bit throughput vs key length, log-log](measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_128.svg) - - - -#### 64-bit one-shot throughput (ns/op, mean of the 3 fastest of 9 reps; lower is better) - -| Length | mumbo | rapidhash | xxh3 | xxh64 | murmur3 | siphash24 | fnv1a | dumbo | -| -----: | -------- | --------- | -------- | ----- | ------- | --------- | -------- | ----- | -| 1B | 1.39 | 1.71 | 1.09 | 1.50 | 2.39 | 5.29 | **0.36** | 1.12 | -| 3B | 1.48 | 1.65 | 1.09 | 2.10 | 2.85 | 6.05 | **0.73** | 1.57 | -| 5B | 1.30 | 1.65 | **0.99** | 1.93 | 2.58 | 5.13 | 1.17 | 1.26 | -| 7B | 1.30 | 1.65 | **0.99** | 2.60 | 2.58 | 5.13 | 1.56 | 1.26 | -| 8B | 1.30 | 1.47 | **0.99** | 1.94 | 2.58 | 6.51 | 1.78 | 1.26 | -| 11B | 1.30 | 1.47 | **0.79** | 3.01 | 3.40 | 7.70 | 2.56 | 1.90 | -| 15B | 1.30 | 1.47 | **0.79** | 3.55 | 3.00 | 7.17 | 3.73 | 1.40 | -| 16B | 1.30 | 1.47 | **0.79** | 2.36 | 2.77 | 8.04 | 4.04 | 1.40 | -| 19B | 1.84 | 1.84 | **1.62** | 3.49 | 3.87 | 8.64 | 4.99 | 2.27 | -| 22B | 1.84 | 1.84 | **1.62** | 3.65 | 3.55 | 8.23 | 6.07 | 1.78 | -| 27B | 1.83 | 1.84 | **1.62** | 4.07 | 4.51 | 10.10 | 8.12 | 2.66 | -| 32B | 1.84 | 1.84 | **1.62** | 4.22 | 3.68 | 11.26 | 10.21 | 2.25 | -| 38B | 2.02 | **2.02** | 2.52 | 5.82 | 4.50 | 11.31 | 12.75 | 2.68 | -| 47B | **2.02** | 2.02 | 2.52 | 7.09 | 4.94 | 12.93 | 18.11 | 3.28 | -| 48B | 2.02 | **2.02** | 2.52 | 5.65 | 4.67 | 14.49 | 18.80 | 3.25 | -| 63B | 2.30 | **2.19** | 2.52 | 8.55 | 5.79 | 16.20 | 26.28 | 4.28 | -| 64B | 2.29 | **2.19** | 2.52 | 5.26 | 5.60 | 17.71 | 26.85 | 4.26 | -| 127B | 4.21 | **3.87** | 4.37 | 11.15 | 9.54 | 29.13 | 67.24 | 9.18 | -| 128B | 10.26 | **3.86** | 4.33 | 7.17 | 9.51 | 30.64 | 67.94 | 9.18 | -| 256B | 11.97 | **5.87** | 54.80 | 11.15 | 18.20 | 56.54 | 163.5 | 21.82 | -| 1Ki | 34.99 | **19.31** | 98.09 | 34.90 | 72.59 | 214.2 | 709.8 | 117.0 | -| 4Ki | 133.6 | **72.78** | 319.8 | 130.8 | 294.2 | 835.5 | 2891 | 528.6 | - -#### 128-bit one-shot throughput (ns/op, mean of the 3 fastest of 9 reps; native-128 algorithms only) - -| Length | jumbo | xxh3 | murmur3 | -| -----: | --------- | -------- | ------- | -| 1B | 2.38 | **1.46** | 2.60 | -| 3B | 2.38 | **1.46** | 3.07 | -| 5B | 2.23 | **1.25** | 2.84 | -| 7B | 2.23 | **1.26** | 2.84 | -| 8B | 2.21 | **1.25** | 2.84 | -| 11B | 2.20 | **1.64** | 3.61 | -| 15B | 2.21 | **1.65** | 3.19 | -| 16B | 2.21 | **1.65** | 3.04 | -| 19B | 2.78 | **2.24** | 4.06 | -| 22B | 2.78 | **2.24** | 3.80 | -| 27B | 2.78 | **2.24** | 4.66 | -| 32B | 2.78 | **2.24** | 3.91 | -| 38B | 3.46 | **3.15** | 4.70 | -| 47B | 3.46 | **3.15** | 5.10 | -| 48B | 3.46 | **3.15** | 4.87 | -| 63B | 4.00 | **3.14** | 6.06 | -| 64B | 4.68 | **3.14** | 5.86 | -| 127B | 6.74 | **5.06** | 9.80 | -| 128B | 5.76 | **5.02** | 9.81 | -| 256B | **8.01** | 57.73 | 18.48 | -| 1Ki | **22.50** | 100.8 | 72.88 | -| 4Ki | **81.15** | 322.9 | 294.0 | - -#### Mixed-length latency (ns/hash, mean of the 3 fastest of 9 reps; lower is better) - -| max len | mumbo | rapidhash | xxh3 | xxh64 | murmur3 | siphash24 | fnv1a | dumbo | -| ------: | ----- | --------- | ----- | ----- | ------- | --------- | -------- | ----- | -| 16 | 4.29 | **4.12** | 4.29 | 7.18 | 6.68 | 8.48 | 6.88 | 5.78 | -| 64 | 5.21 | 5.49 | 1.57 | 13.48 | 8.23 | 11.86 | **0.27** | 8.22 | -| 1024 | 22.29 | **9.93** | 49.64 | 39.52 | 44.07 | 112.2 | 331.3 | 59.00 | + + +### AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8 + + + +![mbo/hash 64-bit latency, AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8](measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency64.svg) + +#### 64-bit latency (ns/hash at exact length, mean of the 3 best of 9 reps; lower is better) + +| Length | mumbo | rapidhash | xxh3 | xxh64 | murmur3 | siphash24 | fnv1a | dumbo | +| -----: | ----: | --------: | -------: | ----: | ------: | --------: | -------: | ----: | +| 1 B | 1.38 | 1.70 | 1.08 | 1.49 | 2.37 | 5.33 | **0.36** | 1.12 | +| 3 B | 1.47 | 1.65 | 1.08 | 2.10 | 2.83 | 5.99 | **0.72** | 1.56 | +| 5 B | 1.29 | 1.65 | **0.98** | 1.93 | 2.57 | 5.20 | 1.26 | 1.25 | +| 7 B | 1.29 | 1.70 | **0.99** | 2.59 | 2.57 | 5.15 | 1.55 | 1.25 | +| 8 B | 1.31 | 1.46 | **0.98** | 1.93 | 2.57 | 6.48 | 1.78 | 1.25 | +| 11 B | 1.30 | 1.46 | **0.79** | 2.99 | 3.38 | 7.67 | 2.55 | 1.90 | +| 15 B | 1.29 | 1.46 | **0.79** | 3.53 | 2.98 | 7.15 | 3.72 | 1.40 | +| 16 B | 1.29 | 1.47 | **0.79** | 2.36 | 2.76 | 8.00 | 4.04 | 1.40 | +| 19 B | 1.83 | 1.83 | **1.62** | 3.47 | 3.85 | 8.59 | 4.99 | 2.26 | +| 22 B | 1.86 | 1.83 | **1.62** | 3.63 | 3.54 | 8.19 | 6.09 | 1.78 | +| 27 B | 1.83 | 1.83 | **1.62** | 4.04 | 4.49 | 10.05 | 8.10 | 2.66 | +| 32 B | 1.84 | 1.83 | **1.62** | 4.20 | 3.66 | 11.22 | 10.21 | 2.24 | +| 38 B | 2.02 | **2.01** | 2.51 | 5.81 | 4.47 | 11.27 | 12.71 | 2.68 | +| 47 B | 2.02 | **2.01** | 2.54 | 7.07 | 4.91 | 12.88 | 18.22 | 3.27 | +| 48 B | 2.02 | **2.01** | 2.51 | 5.63 | 4.65 | 14.46 | 18.86 | 3.24 | +| 63 B | 2.29 | **2.19** | 2.51 | 8.59 | 5.76 | 16.16 | 26.38 | 4.23 | +| 64 B | 2.29 | **2.18** | 2.51 | 5.24 | 5.58 | 17.69 | 26.84 | 4.24 | +| 127 B | 4.19 | **3.86** | 4.34 | 11.13 | 9.50 | 29.09 | 67.19 | 9.19 | +| 128 B | 9.98 | **3.85** | 4.31 | 7.14 | 9.48 | 30.60 | 67.82 | 9.17 | +| 256 B | 11.85 | **5.85** | 55.39 | 11.11 | 18.18 | 56.41 | 163.2 | 21.87 | +| 1 KiB | 34.91 | **19.25** | 98.66 | 34.75 | 72.44 | 213.9 | 708.3 | 116.9 | +| 4 KiB | 133.5 | **72.43** | 320.9 | 130.3 | 293.2 | 834.5 | 2886 | 527.5 | + +![mbo/hash 64-bit throughput (Short), AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8](measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Short.svg) + +#### 64-bit throughput, Short lengths (GiB/s over lengths <= max, mean of the 3 best of 9 reps; higher is better) + +| max len | mumbo | rapidhash | xxh3 | xxh64 | murmur3 | siphash24 | fnv1a | dumbo | +| ------: | ----: | --------: | -------: | ----: | ------: | --------: | ----: | ----: | +| 8 B | 2.41 | 2.04 | **3.12** | 1.65 | 1.29 | 0.63 | 2.92 | 2.60 | +| 12 B | 4.27 | 3.73 | **5.75** | 2.51 | 2.05 | 0.98 | 3.02 | 3.80 | +| 16 B | 6.09 | 5.17 | **8.38** | 3.19 | 2.88 | 1.33 | 3.12 | 5.39 | +| 23 B | 7.27 | 6.37 | **9.13** | 3.84 | 3.51 | 1.61 | 2.98 | 6.31 | +| 31 B | 8.61 | 7.75 | **10.6** | 4.46 | 4.11 | 1.87 | 2.96 | 7.20 | +| 38 B | 8.26 | 7.69 | **10.1** | 4.37 | 4.08 | 1.84 | 2.82 | 7.04 | +| 53 B | 9.11 | 8.59 | **10.9** | 4.66 | 4.46 | 2.00 | 2.77 | 7.52 | +| 80 B | 9.14 | 8.58 | **10.9** | 4.70 | 4.47 | 2.00 | 2.74 | 7.47 | +| 128 B | 9.15 | 8.57 | **10.9** | 4.73 | 4.47 | 2.00 | 2.74 | 7.47 | + +![mbo/hash 64-bit throughput (Web), AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8](measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Web.svg) + +#### 64-bit throughput, Web lengths (GiB/s over lengths <= max, mean of the 3 best of 9 reps; higher is better) + +| max len | mumbo | rapidhash | xxh3 | xxh64 | murmur3 | siphash24 | fnv1a | dumbo | +| ------: | ----: | --------: | -------: | ----: | ------: | --------: | ----: | ----: | +| 15 B | 4.82 | 4.21 | **6.54** | 2.80 | 2.39 | 1.14 | 3.26 | 4.53 | +| 28 B | 7.85 | 7.15 | **9.26** | 4.27 | 3.81 | 1.74 | 2.81 | 6.62 | +| 45 B | 11.9 | 11.8 | **12.0** | 5.42 | 5.65 | 2.43 | 2.53 | 8.83 | +| 75 B | 15.8 | **15.9** | 14.6 | 6.46 | 7.00 | 2.86 | 2.14 | 9.88 | +| 120 B | 17.7 | **18.5** | 16.6 | 7.53 | 7.93 | 3.12 | 1.92 | 9.98 | +| 220 B | 17.3 | **19.9** | 17.4 | 8.18 | 8.47 | 3.27 | 1.85 | 10.1 | +| 512 B | 17.7 | **21.4** | 11.3 | 8.98 | 8.82 | 3.36 | 1.74 | 9.47 | +| 2 KiB | 19.0 | **24.3** | 10.9 | 10.5 | 9.53 | 3.55 | 1.63 | 8.93 | +| 4 KiB | 18.7 | **23.8** | 10.4 | 10.4 | 9.40 | 3.51 | 1.63 | 8.94 | + +![mbo/hash 128-bit latency, AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8](measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency128.svg) + +#### 128-bit latency (ns/hash at exact length, mean of the 3 best of 9 reps; native-128 only; lower is better) + +| Length | jumbo | xxh3 | murmur3 | +| -----: | --------: | -------: | ------: | +| 1 B | 2.37 | **1.45** | 2.58 | +| 3 B | 2.38 | **1.45** | 3.05 | +| 5 B | 2.22 | **1.25** | 2.82 | +| 7 B | 2.22 | **1.25** | 2.82 | +| 8 B | 2.21 | **1.25** | 2.82 | +| 11 B | 2.20 | **1.64** | 3.59 | +| 15 B | 2.20 | **1.64** | 3.16 | +| 16 B | 2.20 | **1.64** | 3.02 | +| 19 B | 2.77 | **2.23** | 4.04 | +| 22 B | 2.77 | **2.23** | 3.78 | +| 27 B | 2.77 | **2.23** | 4.64 | +| 32 B | 2.77 | **2.23** | 3.89 | +| 38 B | 3.45 | **3.16** | 4.67 | +| 47 B | 3.44 | **3.13** | 5.07 | +| 48 B | 3.45 | **3.13** | 4.85 | +| 63 B | 3.98 | **3.12** | 6.03 | +| 64 B | 4.66 | **3.13** | 5.83 | +| 127 B | 6.71 | **5.04** | 9.76 | +| 128 B | 5.74 | **5.00** | 9.78 | +| 256 B | **7.97** | 58.27 | 18.47 | +| 1 KiB | **22.36** | 101.2 | 72.76 | +| 4 KiB | **80.79** | 322.4 | 293.4 | + +### Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8 + + + +![mbo/hash 64-bit latency, Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8](measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency64.svg) + +#### 64-bit latency (ns/hash at exact length, mean of the 3 best of 9 reps; lower is better) + +| Length | mumbo | rapidhash | xxh3 | xxh64 | murmur3 | siphash24 | fnv1a | dumbo | +| -----: | --------: | --------: | ----: | ----: | ------: | --------: | -------: | -------: | +| 1 B | 2.20 | 2.00 | 1.83 | 2.06 | 2.74 | 6.49 | **0.49** | 1.58 | +| 3 B | 2.33 | 1.99 | 1.94 | 2.73 | 2.99 | 6.58 | **1.04** | 2.84 | +| 5 B | 2.58 | 1.74 | 1.63 | 2.12 | 2.99 | 6.74 | 1.81 | **1.49** | +| 7 B | 2.59 | 1.74 | 1.64 | 2.82 | 2.96 | 6.90 | 2.66 | **1.47** | +| 8 B | 2.08 | 1.74 | 1.61 | 2.26 | 3.11 | 9.16 | 2.85 | **1.52** | +| 11 B | 2.03 | **1.73** | 1.73 | 3.23 | 3.51 | 9.36 | 4.41 | 2.71 | +| 15 B | 1.96 | **1.74** | 1.74 | 3.64 | 3.57 | 9.35 | 6.15 | 1.93 | +| 16 B | 2.03 | **1.74** | 1.74 | 2.73 | 3.97 | 12.16 | 6.52 | 1.88 | +| 19 B | 2.44 | **2.03** | 2.39 | 3.84 | 4.45 | 12.31 | 7.60 | 3.13 | +| 22 B | 2.44 | **1.97** | 2.39 | 4.00 | 4.38 | 12.31 | 8.99 | 2.47 | +| 27 B | 2.44 | **2.02** | 2.39 | 4.74 | 5.15 | 15.38 | 11.63 | 3.68 | +| 32 B | 2.44 | **2.00** | 2.39 | 5.09 | 5.43 | 18.44 | 15.37 | 3.12 | +| 38 B | 2.84 | **2.43** | 3.79 | 6.88 | 5.92 | 18.66 | 19.29 | 3.67 | +| 47 B | 2.84 | **2.44** | 3.79 | 8.52 | 6.32 | 21.84 | 27.47 | 4.26 | +| 48 B | 2.83 | **2.43** | 3.78 | 6.70 | 6.84 | 25.14 | 28.41 | 4.25 | +| 63 B | 3.36 | **2.96** | 3.79 | 10.31 | 7.99 | 28.70 | 40.24 | 5.78 | +| 64 B | 3.29 | **2.93** | 3.78 | 6.65 | 8.58 | 32.04 | 41.04 | 5.66 | +| 127 B | 6.04 | **4.86** | 6.50 | 13.78 | 15.04 | 57.91 | 117.3 | 12.74 | +| 128 B | 6.55 | **4.87** | 6.51 | 9.64 | 15.95 | 61.38 | 118.4 | 12.57 | +| 256 B | 8.69 | **7.44** | 26.58 | 16.21 | 33.49 | 121.0 | 287.1 | 31.04 | +| 1 KiB | 23.74 | **22.48** | 58.64 | 55.42 | 165.2 | 476.6 | 1354 | 194.6 | +| 4 KiB | **82.36** | 83.87 | 175.4 | 225.4 | 694.5 | 1915 | 5610 | 882.1 | + +![mbo/hash 64-bit throughput (Short), Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8](measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Short.svg) + +#### 64-bit throughput, Short lengths (GiB/s over lengths <= max, mean of the 3 best of 9 reps; higher is better) + +| max len | mumbo | rapidhash | xxh3 | xxh64 | murmur3 | siphash24 | fnv1a | dumbo | +| ------: | ----: | --------: | -------: | ----: | ------: | --------: | ----: | -------: | +| 8 B | 0.90 | 0.72 | 1.34 | 0.91 | 0.62 | 0.51 | 1.45 | **1.51** | +| 12 B | 1.85 | 1.44 | **2.26** | 1.45 | 1.06 | 0.75 | 1.61 | 2.15 | +| 16 B | 2.63 | 2.45 | 3.57 | 2.01 | 2.19 | 1.01 | 1.55 | **3.64** | +| 23 B | 3.34 | 3.44 | **3.89** | 2.27 | 2.46 | 1.17 | 1.40 | 3.70 | +| 31 B | 3.89 | 4.48 | **4.58** | 2.70 | 2.84 | 1.32 | 1.41 | 4.44 | +| 38 B | 3.78 | 4.40 | **4.56** | 2.64 | 2.75 | 1.30 | 1.33 | 4.29 | +| 53 B | 4.08 | 4.65 | **4.85** | 2.91 | 2.95 | 1.38 | 1.34 | 4.53 | +| 80 B | 4.08 | 4.71 | **4.93** | 2.91 | 2.94 | 1.38 | 1.37 | 4.73 | +| 128 B | 4.04 | 4.44 | **4.84** | 2.90 | 2.92 | 1.38 | 1.34 | 4.49 | + +![mbo/hash 64-bit throughput (Web), Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8](measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Web.svg) + +#### 64-bit throughput, Web lengths (GiB/s over lengths <= max, mean of the 3 best of 9 reps; higher is better) + +| max len | mumbo | rapidhash | xxh3 | xxh64 | murmur3 | siphash24 | fnv1a | dumbo | +| ------: | -------: | --------: | -------: | ----: | ------: | --------: | ----: | ----: | +| 15 B | 2.04 | 1.56 | **2.58** | 1.55 | 1.55 | 0.88 | 1.82 | 2.47 | +| 28 B | 3.32 | 3.94 | **3.99** | 2.53 | 2.50 | 1.22 | 1.34 | 3.86 | +| 45 B | 5.52 | **6.19** | 5.95 | 3.65 | 3.98 | 1.57 | 1.27 | 5.83 | +| 75 B | 7.41 | 6.66 | **8.09** | 4.70 | 5.05 | 1.75 | 1.27 | 7.29 | +| 120 B | 8.85 | 9.09 | **9.61** | 5.59 | 5.54 | 1.81 | 1.16 | 7.64 | +| 220 B | 9.90 | **10.3** | 9.92 | 6.21 | 5.80 | 1.85 | 1.11 | 7.65 | +| 512 B | **11.0** | 10.9 | 9.48 | 6.63 | 5.68 | 1.85 | 1.02 | 7.01 | +| 2 KiB | 13.0 | **13.5** | 10.5 | 7.64 | 5.71 | 1.88 | 0.92 | 6.28 | +| 4 KiB | 12.5 | **13.4** | 10.2 | 7.53 | 5.71 | 1.87 | 0.93 | 6.34 | + +![mbo/hash 128-bit latency, Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8](measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency128.svg) + +#### 128-bit latency (ns/hash at exact length, mean of the 3 best of 9 reps; native-128 only; lower is better) + +| Length | jumbo | xxh3 | murmur3 | +| -----: | --------: | -------: | ------: | +| 1 B | 3.12 | **2.32** | 2.75 | +| 3 B | 3.32 | **2.37** | 2.99 | +| 5 B | 3.12 | **2.22** | 3.01 | +| 7 B | 3.04 | **2.24** | 3.01 | +| 8 B | 2.93 | **2.23** | 3.10 | +| 11 B | 2.94 | **2.67** | 3.52 | +| 15 B | 2.93 | **2.67** | 3.53 | +| 16 B | 2.94 | **2.67** | 3.96 | +| 19 B | 3.83 | **3.47** | 4.46 | +| 22 B | 3.82 | **3.46** | 4.40 | +| 27 B | 3.82 | **3.46** | 5.09 | +| 32 B | 3.82 | **3.48** | 5.56 | +| 38 B | **4.44** | 4.97 | 5.86 | +| 47 B | **4.43** | 4.98 | 6.50 | +| 48 B | **4.45** | 4.99 | 6.92 | +| 63 B | 5.30 | **4.98** | 8.01 | +| 64 B | 6.18 | **4.98** | 8.59 | +| 127 B | 8.73 | **8.02** | 14.92 | +| 128 B | **7.45** | 8.02 | 16.09 | +| 256 B | **10.57** | 28.01 | 33.26 | +| 1 KiB | **31.59** | 60.37 | 164.7 | +| 4 KiB | **123.3** | 177.5 | 699.2 | -Reading the tables (exact numbers are per machine above; the pattern holds across -arm64/clang and x86_64/gcc): `mumbo` and `rapidhash` are close on small keys. -`mumbo` gives up a little on 17-64 byte keys - the cost of the extra finalizer -that gets it a clean SMHasher3 pass - but wins the mixed-length latency test, -which is closer to how a hash table actually uses a hash. For 128-bit output, -`jumbo` is fastest from the mid sizes up. `fnv1a` is quickest on 1-3 byte keys, -`dumbo` does well on tiny keys but falls off on large ones, and `siphash` is -slower throughout - the price of being a keyed PRF (Pseudo-Random Function, see +### Reading the results + +Exact numbers are per machine above; the pattern holds across arm64/clang and +x86_64/gcc. In **latency** (exact length, ns), `fnv1a` is quickest on 1-3 byte +keys and `xxh3` across the small-to-mid range, with `mumbo` and `rapidhash` close +behind; `mumbo` gives up a little on the 17-64 byte keys - the cost of the extra +finalizer that earns its clean SMHasher3 pass - and `siphash` trails throughout, +the price of being a keyed PRF (Pseudo-Random Function, see [SipHash: a fast short-input PRF](https://cr.yp.to/siphash/siphash-20120918.pdf)). +In **throughput** (a realistic bounded mix, GiB/s), `rapidhash` leads as the +upper bound grows into bulk with `mumbo` close behind, `dumbo` is strong on the +short (identifier) mix but falls off on the long (web) one, and `fnv1a` and +`siphash` trail on the larger mixes. For 128-bit output, `xxh3` edges `jumbo` on +latency, but `jumbo` is the only native 128-bit hash here that passes SMHasher3 +clean. ## Quality: SMHasher3 @@ -465,18 +524,18 @@ numbers are directly comparable. -| Algorithm | Bits | Role in mbo/hash | SMHasher3 result | Failures | -| ----------- | ---: | ------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `dumbo` | 64 | `hash.h` (compact MUM) | PASS | none | -| `fnv1a` | 64 | `hash.h` | 7/186 | nearly every family: Avalanche, BIC, Sparse, Cyclic, Permutation, Text, TwoBytes, Bitflip, PerlinNoise, and the complete Seed* cluster | -| `mumbo` | 64 | default (64/32/streaming) | PASS | none | -| `rapidhash` | 64 | extra (`hash_extra_cc`) | PASS | none | -| `siphash` | 64 | `hash.h` (keyed PRF) | PASS | none | -| `xxh3` | 64 | extra (`hash_extra_cc`) | 166/188 | BIC [3, 8, 11], Sparse [20/3], PerlinNoise [2], Bitflip [8], SeedZeroes [1280, 8448], SeedSparse [2, 3], SeedBlockLen [8, 13, 14, 15, 16], SeedBlockOffset [0, 1, 2, 3, 4], SeedBIC [3, 8] | -| `xxh64` | 64 | extra (`hash_extra_cc`) | 181/188 | SeedBlockLen [15, 19, 21, 26, 29, 30], SeedBIC [8] | -| `jumbo` | 128 | default (128) | PASS | none | -| `murmur3` | 128 | `hash.h` | 123/188 | BIC, Zeroes, Permutation, and the complete Seed* cluster (11 families) | -| `xxh3` | 128 | extra (`hash_extra_cc`) | 162/188 | BIC [3, 8, 15], Sparse [20/3], PerlinNoise [2], Bitflip [3, 4, 8], SeedZeroes [1280, 8448], SeedSparse [2, 3], SeedBlockLen [8, 12, 13, 14, 15, 16], SeedBlockOffset [0, 1, 2, 3, 4, 5], SeedBIC [3, 8] | +| Algorithm | Bits | Role in mbo/hash | SMHasher3 result | Failures | +| ----------- | ---: | ------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `dumbo` | 64 | `hash.h` (compact MUM) | PASS | none | +| `fnv1a` | 64 | `hash.h` | 7/186 | Avalanche [3, 4, 5, 6, 7, 8, 9, 10, 12, 16, 20, 64, 128], BIC [3, 8, 11, 15], Zeroes [], Cyclic [4 cycles of 3 bytes, 4 cycles of 4 bytes, 4 cycles of 5 bytes, 4 cycles of 8 bytes, 8 cycles of 3 bytes, 8 cycles of 4 bytes, 8 cycles of 5 bytes, 8 cycles of 8 bytes, 12 cycles of 3 bytes, 12 cycles of 4 bytes, 12 cycles of 5 bytes, 12 cycles of 8 bytes, 16 cycles of 3 bytes, 16 cycles of 4 bytes, 16 cycles of 5 bytes, 16 cycles of 8 bytes], Sparse [6/2, 4/3, 4/4, 4/5, 3/6, 3/7, 3/8, 3/9, 3/10, 3/12, 3/14, 10/2, 20/3, 9/4, 5/9, 4/14, 4/16, 3/32, 3/48, 3/64, 3/96, 2/128, 2/256, 2/512, 2/1024, 2/1280] | +| `mumbo` | 64 | default (64/32/streaming) | PASS | none | +| `rapidhash` | 64 | extra (`hash_extra_cc`) | PASS | none | +| `siphash` | 64 | `hash.h` (keyed PRF) | PASS | none | +| `xxh3` | 64 | extra (`hash_extra_cc`) | 166/188 | BIC [3, 8, 11], Sparse [20/3], PerlinNoise [2], Bitflip [8], SeedZeroes [1280, 8448], SeedSparse [2, 3], SeedBlockLen [8, 13, 14, 15, 16], SeedBlockOffset [0, 1, 2, 3, 4], SeedBIC [3, 8] | +| `xxh64` | 64 | extra (`hash_extra_cc`) | 181/188 | SeedBlockLen [15, 19, 21, 26, 29, 30], SeedBIC [8] | +| `jumbo` | 128 | default (128) | PASS | none | +| `murmur3` | 128 | `hash.h` | 123/188 | BIC [3], Zeroes [] | +| `xxh3` | 128 | extra (`hash_extra_cc`) | 162/188 | BIC [3, 8, 15], Sparse [20/3], PerlinNoise [2], Bitflip [3, 4, 8], SeedZeroes [1280, 8448], SeedSparse [2, 3], SeedBlockLen [8, 12, 13, 14, 15, 16], SeedBlockOffset [0, 1, 2, 3, 4, 5], SeedBIC [3, 8] | diff --git a/mbo/hash/hash_benchmark.cc b/mbo/hash/hash_benchmark.cc index 6444cae..a178068 100644 --- a/mbo/hash/hash_benchmark.cc +++ b/mbo/hash/hash_benchmark.cc @@ -157,6 +157,7 @@ std::span ThroughputSizes() { } template +requires HasGetHash64 void BmHash64(benchmark::State& state) { const auto length = static_cast(state.range(0)); // NOLINTNEXTLINE(cert-msc51-cpp,cert-msc32-c,bugprone-random-generator-seed): fixed data per length @@ -285,6 +286,7 @@ const std::vector& ThroughputKeys(std::size_t dist_index, std::size } template +requires HasGetHash64 void BmHash64Throughput(benchmark::State& state, std::size_t dist_index, std::size_t bound_index) { const std::vector& keys = ThroughputKeys(dist_index, bound_index); int64_t total_bytes = 0; @@ -301,31 +303,62 @@ void BmHash64Throughput(benchmark::State& state, std::size_t dist_index, std::si state.SetLabel(std::string(Algo::Name())); } -// Registers the 64-bit benchmark for one algorithm, plus the 128-bit one where -// the descriptor provides it (detected via the public HasGetHash128 concept). +template +requires HasGetHash128 +void BmHash128Throughput(benchmark::State& state, std::size_t dist_index, std::size_t bound_index) { + const std::vector& keys = ThroughputKeys(dist_index, bound_index); + int64_t total_bytes = 0; + for (const std::string& key : keys) { + total_bytes += static_cast(key.size()); + } + std::size_t counter = 0; + for (auto _ : state) { + benchmark::DoNotOptimize(Algo::GetHash128(keys[counter++ & (kLatencyKeys - 1)], kSeed)); + } + state.SetItemsProcessed(state.iterations()); + state.SetBytesProcessed(state.iterations() * (total_bytes / static_cast(kLatencyKeys))); + state.SetLabel(std::string(Algo::Name())); +} + +// Registers the 64-bit benchmarks (latency + throughput) for an algorithm that +// exposes GetHash64, and the 128-bit ones for an algorithm that exposes +// GetHash128 - so a 64-only or a 128-only algorithm is handled correctly. template void RegisterAlgo() { const std::string name(Algo::Name()); const std::span sizes = ThroughputSizes(); - auto* const hash64 = benchmark::RegisterBenchmark(absl::StrCat("BmHash64<", name, ">"), BmHash64); - for (const int size : sizes) { - hash64->Arg(size); + // Throughput over upper-bounded length ranges: one benchmark per (distribution, + // bound), named "BmHash{64,128}Throughput/:", each + // reporting bytes/s. Sweeping the bounds gives the upper-length -> throughput + // curve; the exact-length BmHash{64,128} give the latency curve. + if constexpr (HasGetHash64) { + auto* const hash64 = benchmark::RegisterBenchmark(absl::StrCat("BmHash64<", name, ">"), BmHash64); + for (const int size : sizes) { + hash64->Arg(size); + } + for (std::size_t dist = 0; dist < kLatencyDists.size(); ++dist) { + for (std::size_t bound = 0; bound < kCdfPoints; ++bound) { + benchmark::RegisterBenchmark( + absl::StrCat( + "BmHash64Throughput<", name, ">/", kLatencyDists[dist].name, ":", + kLatencyDists[dist].cdf[bound].second), + [dist, bound](benchmark::State& state) { BmHash64Throughput(state, dist, bound); }); + } + } } if constexpr (HasGetHash128) { auto* const hash128 = benchmark::RegisterBenchmark(absl::StrCat("BmHash128<", name, ">"), BmHash128); for (const int size : sizes) { hash128->Arg(size); } - } - // Throughput over upper-bounded length ranges: one benchmark per (distribution, - // bound), named "BmHash64Throughput/:", each reporting - // bytes/s. Sweeping the bounds gives the upper-length -> throughput curve. - for (std::size_t dist = 0; dist < kLatencyDists.size(); ++dist) { - for (std::size_t bound = 0; bound < kCdfPoints; ++bound) { - benchmark::RegisterBenchmark( - absl::StrCat( - "BmHash64Throughput<", name, ">/", kLatencyDists[dist].name, ":", kLatencyDists[dist].cdf[bound].second), - [dist, bound](benchmark::State& state) { BmHash64Throughput(state, dist, bound); }); + for (std::size_t dist = 0; dist < kLatencyDists.size(); ++dist) { + for (std::size_t bound = 0; bound < kCdfPoints; ++bound) { + benchmark::RegisterBenchmark( + absl::StrCat( + "BmHash128Throughput<", name, ">/", kLatencyDists[dist].name, ":", + kLatencyDists[dist].cdf[bound].second), + [dist, bound](benchmark::State& state) { BmHash128Throughput(state, dist, bound); }); + } } } } diff --git a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_128.svg b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_128.svg deleted file mode 100644 index 2f54b54..0000000 --- a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_128.svg +++ /dev/null @@ -1,40 +0,0 @@ - - -mbo/hash - 128-bit one-shot throughput -AMD Ryzen 9 9950X 16-Core Processor · x86_64 · 10-core · gcc-15 · 1417af5e - -10 - -100 - -1B - - -4B - - -16B - - -64B - - -256B - - -1Ki - - -4Ki -key length (log scale) -ns / op (log scale) - - -jumbo - - -xxh3 - - -murmur3 - diff --git a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_64.svg b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_64.svg deleted file mode 100644 index 34768a0..0000000 --- a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_64.svg +++ /dev/null @@ -1,59 +0,0 @@ - - -mbo/hash - 64-bit one-shot throughput -AMD Ryzen 9 9950X 16-Core Processor · x86_64 · 10-core · gcc-15 · 1417af5e - -1 - -10 - -100 - -1000 - -1B - - -4B - - -16B - - -64B - - -256B - - -1Ki - - -4Ki -key length (log scale) -ns / op (log scale) - - -mumbo - - -rapidhash - - -xxh3 - - -xxh64 - - -murmur3 - - -siphash24 - - -fnv1a - - -dumbo - diff --git a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency128.svg b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency128.svg new file mode 100644 index 0000000..47d4ab0 --- /dev/null +++ b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency128.svg @@ -0,0 +1,40 @@ + + +mbo/hash - 128-bit latency +AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8 + +10 + +100 + +1 B + + +4 B + + +16 B + + +64 B + + +256 B + + +1 KiB + + +4 KiB +key length (log scale) +ns / op (log scale) + + +jumbo + + +xxh3 + + +murmur3 + diff --git a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency64.svg b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency64.svg new file mode 100644 index 0000000..acb5744 --- /dev/null +++ b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency64.svg @@ -0,0 +1,59 @@ + + +mbo/hash - 64-bit latency +AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8 + +1 + +10 + +100 + +1000 + +1 B + + +4 B + + +16 B + + +64 B + + +256 B + + +1 KiB + + +4 KiB +key length (log scale) +ns / op (log scale) + + +mumbo + + +rapidhash + + +xxh3 + + +xxh64 + + +murmur3 + + +siphash24 + + +fnv1a + + +dumbo + diff --git a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Short.svg b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Short.svg new file mode 100644 index 0000000..912dc3c --- /dev/null +++ b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Short.svg @@ -0,0 +1,42 @@ + + +mbo/hash - 64-bit throughput (Short) +AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8 + +1 + +10 + + +16 B + + +64 B + +max length (log scale) +GiB / s (log scale) + + +mumbo + + +rapidhash + + +xxh3 + + +xxh64 + + +murmur3 + + +siphash24 + + +fnv1a + + +dumbo + diff --git a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Web.svg b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Web.svg new file mode 100644 index 0000000..3d68e6b --- /dev/null +++ b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Web.svg @@ -0,0 +1,47 @@ + + +mbo/hash - 64-bit throughput (Web) +AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8 + +10 + +16 B + + +64 B + + +256 B + + +1 KiB + + +4 KiB +max length (log scale) +GiB / s (log scale) + + +mumbo + + +rapidhash + + +xxh3 + + +xxh64 + + +murmur3 + + +siphash24 + + +fnv1a + + +dumbo + diff --git a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency128.svg b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency128.svg new file mode 100644 index 0000000..4fce194 --- /dev/null +++ b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency128.svg @@ -0,0 +1,40 @@ + + +mbo/hash - 128-bit latency +Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8 + +10 + +100 + +1 B + + +4 B + + +16 B + + +64 B + + +256 B + + +1 KiB + + +4 KiB +key length (log scale) +ns / op (log scale) + + +jumbo + + +xxh3 + + +murmur3 + diff --git a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency64.svg b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency64.svg new file mode 100644 index 0000000..1be820c --- /dev/null +++ b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency64.svg @@ -0,0 +1,59 @@ + + +mbo/hash - 64-bit latency +Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8 + +1 + +10 + +100 + +1000 + +1 B + + +4 B + + +16 B + + +64 B + + +256 B + + +1 KiB + + +4 KiB +key length (log scale) +ns / op (log scale) + + +mumbo + + +rapidhash + + +xxh3 + + +xxh64 + + +murmur3 + + +siphash24 + + +fnv1a + + +dumbo + diff --git a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Short.svg b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Short.svg new file mode 100644 index 0000000..08e3604 --- /dev/null +++ b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Short.svg @@ -0,0 +1,40 @@ + + +mbo/hash - 64-bit throughput (Short) +Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8 + +1 + + +16 B + + +64 B + +max length (log scale) +GiB / s (log scale) + + +mumbo + + +rapidhash + + +xxh3 + + +xxh64 + + +murmur3 + + +siphash24 + + +fnv1a + + +dumbo + diff --git a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Web.svg b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Web.svg new file mode 100644 index 0000000..1b305e3 --- /dev/null +++ b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Web.svg @@ -0,0 +1,49 @@ + + +mbo/hash - 64-bit throughput (Web) +Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8 + +1 + +10 + +16 B + + +64 B + + +256 B + + +1 KiB + + +4 KiB +max length (log scale) +GiB / s (log scale) + + +mumbo + + +rapidhash + + +xxh3 + + +xxh64 + + +murmur3 + + +siphash24 + + +fnv1a + + +dumbo + diff --git a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-22_128.svg b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-22_128.svg deleted file mode 100644 index d1478d1..0000000 --- a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-22_128.svg +++ /dev/null @@ -1,40 +0,0 @@ - - -mbo/hash - 128-bit one-shot throughput -Apple M5 Pro · arm64 · 18-core · clang-22 · 1417af5e - -10 - -100 - -1B - - -4B - - -16B - - -64B - - -256B - - -1Ki - - -4Ki -key length (log scale) -ns / op (log scale) - - -jumbo - - -xxh3 - - -murmur3 - diff --git a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-22_64.svg b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-22_64.svg deleted file mode 100644 index 6c95591..0000000 --- a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-22_64.svg +++ /dev/null @@ -1,59 +0,0 @@ - - -mbo/hash - 64-bit one-shot throughput -Apple M5 Pro · arm64 · 18-core · clang-22 · 1417af5e - -1 - -10 - -100 - -1000 - -1B - - -4B - - -16B - - -64B - - -256B - - -1Ki - - -4Ki -key length (log scale) -ns / op (log scale) - - -mumbo - - -rapidhash - - -xxh3 - - -xxh64 - - -murmur3 - - -siphash24 - - -fnv1a - - -dumbo - diff --git a/mbo/hash/measurements/data/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_10c_gcc-15_1417af5e_20260711_203603.tgz b/mbo/hash/measurements/data/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_10c_gcc-15_1417af5e_20260711_203603.tgz deleted file mode 100644 index a8f8882..0000000 --- a/mbo/hash/measurements/data/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_10c_gcc-15_1417af5e_20260711_203603.tgz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a2d037dade17c1434de088c6054eb7cad61e0cf33dfba845a9a9ab43378cd08f -size 644644 diff --git a/mbo/hash/measurements/data/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_10c_gcc-15_80a4c8a8_20260714_225122.tgz b/mbo/hash/measurements/data/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_10c_gcc-15_80a4c8a8_20260714_225122.tgz new file mode 100644 index 0000000..b47f3ba --- /dev/null +++ b/mbo/hash/measurements/data/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_10c_gcc-15_80a4c8a8_20260714_225122.tgz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c20454c6699c04ddd482c69b75896daf29b345986908c101ab8f8438105cba78 +size 1288521 diff --git a/mbo/hash/measurements/data/macos-arm64-apple-m5-pro_18c_clang-21_80a4c8a8_20260714_233133.tgz b/mbo/hash/measurements/data/macos-arm64-apple-m5-pro_18c_clang-21_80a4c8a8_20260714_233133.tgz new file mode 100644 index 0000000..1b54525 --- /dev/null +++ b/mbo/hash/measurements/data/macos-arm64-apple-m5-pro_18c_clang-21_80a4c8a8_20260714_233133.tgz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4819f142e2117870849e22441627a21e9f68721f7ce16e9c39448b48ba5eb73 +size 689950 diff --git a/mbo/hash/measurements/data/macos-arm64-apple-m5-pro_18c_clang-22_1417af5e_20260711_211526.tgz b/mbo/hash/measurements/data/macos-arm64-apple-m5-pro_18c_clang-22_1417af5e_20260711_211526.tgz deleted file mode 100644 index 498a2a5..0000000 --- a/mbo/hash/measurements/data/macos-arm64-apple-m5-pro_18c_clang-22_1417af5e_20260711_211526.tgz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:233ad31a6aa613838eaf22ba239839b1997c1a11b6b86fc5bbfbc013d7b27922 -size 653076 diff --git a/mbo/hash/measurements/hash_benchmark_report.py b/mbo/hash/measurements/hash_benchmark_report.py index 2f18302..0544be1 100755 --- a/mbo/hash/measurements/hash_benchmark_report.py +++ b/mbo/hash/measurements/hash_benchmark_report.py @@ -70,7 +70,12 @@ _ORDER_128 = ["mumbo", "xxh3", "murmur3"] _LABEL_128 = {"mumbo": "jumbo"} # BmHash128 is the jumbo (128-bit) face -_NAME_RE = re.compile(r"^BmHash(64|128)(Latency)?<([A-Za-z0-9]+)>/(\d+)$") +# Exact-length LATENCY: BmHash64/BmHash128/ - cost of hashing one exact +# length in a hot loop, reported as ns. +_LATENCY_RE = re.compile(r"^BmHash(64|128)<([A-Za-z0-9]+)>/(\d+)$") +# Bounded-range THROUGHPUT: BmHash{64,128}Throughput/: - bytes/s +# over a length distribution truncated to each upper bound (Dist in {Short, Web}). +_THROUGHPUT_RE = re.compile(r"^BmHash(64|128)Throughput<([A-Za-z0-9]+)>/([A-Za-z][A-Za-z0-9-]*):(\d+)$") _BENCHMARK_TARGET = "//mbo/hash:hash_benchmark" # mbo algorithm -> SMHasher3 registration name(s). The in-house mumbo/jumbo/ @@ -209,64 +214,79 @@ def _run_benchmark(mode, reps, min_time, warmup, config=None): return json.loads(out[out.index("{") :]) -def distill(raw, mode): - """Raw google/benchmark JSON -> canonical per-case stats + full context. - - Per (width, algo, length) keeps min/median/mean/stddev/cv/reps computed from - the per-iteration rows (the tool's own aggregate rows are ignored so we - control the statistic). `min` is the headline; the rest support error bars - and flag noisy cases. +def _distill_buckets(raw): + """Parse raw google/benchmark JSON into the canonical buckets; return + (buckets, reps_seen, best_k_seen). Pure - no machine/source context - so a + live distill and an offline re-distill (keeping a bundle's original context) + share it. LATENCY (exact length) keeps ns (real_time); THROUGHPUT (bounded + range) keeps bytes/s (higher is better), keyed by the ":" label. """ - ctx = dict(raw.get("context", {})) - ctx.update(_machine_augment()) - ctx["source"] = _source_provenance() - ctx["measurement"] = {"mode": mode, "aggregate": "best-k mean", "reps": None, "best_k": None} - - # Collect per-iteration times per case. - samples = {"throughput64": {}, "throughput128": {}, "latency": {}} - section = {"64": "throughput64", "128": "throughput128"} + lat = {"latency64": {}, "latency128": {}} + lat_section = {"64": "latency64", "128": "latency128"} + tput = {"throughput64": {}, "throughput128": {}} + tput_section = {"64": "throughput64", "128": "throughput128"} for bench in raw.get("benchmarks", []): if bench.get("run_type") != "iteration": continue - match = _NAME_RE.match(bench["name"]) - if not match: + match = _LATENCY_RE.match(bench["name"]) + if match: + width, algo, length = match.groups() + lat[lat_section[width]].setdefault(algo, {}).setdefault(length, []).append(float(bench["real_time"])) continue - width, latency, algo, length = match.groups() - bucket = "latency" if latency else section[width] - samples[bucket].setdefault(algo, {}).setdefault(length, []).append(float(bench["real_time"])) + match = _THROUGHPUT_RE.match(bench["name"]) + if match: + width, algo, dist, bound = match.groups() + tput[tput_section[width]].setdefault(algo, {}).setdefault(f"{dist}:{bound}", []).append( + float(bench["bytes_per_second"]) + ) - result = {"context": ctx} + buckets = {} reps_seen = 0 best_k_seen = 0 - for bucket, algos in samples.items(): - out = result.setdefault(bucket, {}) + + def best_k_stats(values, lower_is_better): + # mean of the k best reps (k ~= reps/3): for latency the low tail is the + # uncontended cost, for throughput the high tail is. `best` is the + # headline; `best_cv` the spread over the selected k. + nonlocal reps_seen, best_k_seen + reps_seen = max(reps_seen, len(values)) + best_k = max(1, len(values) // 3) + best_k_seen = max(best_k_seen, best_k) + ordered = sorted(values, reverse=not lower_is_better) # best first + best_vals = ordered[:best_k] + best = statistics.fmean(best_vals) + best_sd = statistics.stdev(best_vals) if len(best_vals) > 1 else 0.0 + return { + "best": round(best, 4), + "best_cv": round(best_sd / best, 4) if best else 0.0, + "min": round(min(values), 4), + "max": round(max(values), 4), + "median": round(statistics.median(values), 4), + "mean": round(statistics.fmean(values), 4), + "reps": len(values), + } + + for bucket, algos in lat.items(): + out = buckets.setdefault(bucket, {}) for algo, lengths in algos.items(): for length, times in lengths.items(): - reps_seen = max(reps_seen, len(times)) - # Headline aggregate: mean of the k fastest reps (k ~= reps/3). - # Contention only ever slows a run, so the low tail is the - # uncontended cost; averaging the best k rejects the single- - # sample noise that a pure minimum would keep. - best_k = max(1, len(times) // 3) - best_k_seen = max(best_k_seen, best_k) - ordered = sorted(times) - best_times = ordered[:best_k] - best = statistics.fmean(best_times) - # CV over the SELECTED fastest k (not all reps): how stable the - # reported estimate is. A full-set CV would just report the - # contention we already excluded. - best_sd = statistics.stdev(best_times) if len(best_times) > 1 else 0.0 - out.setdefault(algo, {})[length] = { - "best": round(best, 4), - "best_cv": round(best_sd / best, 4) if best else 0.0, - "min": round(ordered[0], 4), - "median": round(statistics.median(times), 4), - "mean": round(statistics.fmean(times), 4), - "reps": len(times), - } - result["context"]["measurement"]["reps"] = reps_seen - result["context"]["measurement"]["best_k"] = best_k_seen - return result + out.setdefault(algo, {})[length] = best_k_stats(times, lower_is_better=True) + for bucket, algos in tput.items(): + out = buckets.setdefault(bucket, {}) + for algo, cases in algos.items(): + for case, bps in cases.items(): + out.setdefault(algo, {})[case] = best_k_stats(bps, lower_is_better=False) + return buckets, reps_seen, best_k_seen + + +def distill(raw, mode): + """Raw google/benchmark JSON -> canonical per-case stats + full context.""" + ctx = dict(raw.get("context", {})) + ctx.update(_machine_augment()) + ctx["source"] = _source_provenance() + buckets, reps_seen, best_k_seen = _distill_buckets(raw) + ctx["measurement"] = {"mode": mode, "aggregate": "best-k mean", "reps": reps_seen, "best_k": best_k_seen} + return {"context": ctx, **buckets} def _provenance_context(): @@ -507,10 +527,10 @@ def sep(c): return "\n".join([line(headers), "| " + " | ".join(sep(c) for c in range(cols)) + " |"] + [line(r) for r in rows]) -def _throughput_table(data, preferred, relabel, sizes=None): - # Length-per-row, algorithm-per-column; each row's bold marks the fastest - # algorithm at that length. `sizes` is the curated README subset, taken from - # the dataset's own `readme_sizes` context (emitted by the benchmark = the C++ +def _length_table(data, preferred, relabel, sizes=None): + # LATENCY table: length-per-row, algorithm-per-column, ns; each row's bold + # marks the fastest (lowest) algorithm at that length. `sizes` is the curated + # README subset from the dataset's own `readme_sizes` context (the C++ # kReadmeSizes), so the small table is extracted from the FULL run with no # second size list to drift. Without it, every measured length is shown. algos = _order(list(data), preferred) @@ -518,7 +538,7 @@ def _throughput_table(data, preferred, relabel, sizes=None): sizes = sorted({int(s) for a in data.values() for s in a}) sizes = [s for s in sizes if any(str(s) in data[a] for a in algos)] headers = ["Length"] + [relabel.get(a, a) for a in algos] - aligns = ["r"] + ["r"] * (len(algos)) + aligns = ["r"] + ["r"] * len(algos) rows = [] for size in sizes: row = {a: _val(data[a][str(size)]) for a in algos if str(size) in data[a]} @@ -536,53 +556,120 @@ def _throughput_table(data, preferred, relabel, sizes=None): return _md_table(headers, rows, aligns) -def _latency_table(data, sizes=None): - # Like _throughput_table, the README latency table shows the curated - # `readme_sizes` subset (the latency benchmark now sweeps the full size set, - # so without this it would dump every length). Falls back to all measured - # lengths when no curated subset is available. - algos = _order(list(data), _ORDER_64) - if sizes is None: - sizes = sorted({int(s) for a in data.values() for s in a}) - sizes = [s for s in sizes if any(str(s) in data[a] for a in algos)] - mins = {s: min(_val(data[a][str(s)]) for a in algos if str(s) in data[a]) for s in sizes} - headers = ["max len"] + algos - aligns = ["r"] + ["r"] * (len(algos)) +def _fmt_gibs(bytes_per_second): + """bytes/s -> GiB/s string (2/1/0 decimals by magnitude).""" + gibs = bytes_per_second / (1024.0**3) + if gibs < 10: + return f"{gibs:.2f}" + if gibs < 100: + return f"{gibs:.1f}" + return f"{gibs:.0f}" + + +def _dists(data): + """Distribution names present in a throughput bucket, in Short-then-Web order.""" + seen = [] + for algo in data.values(): + for key in algo: + name = key.split(":", 1)[0] + if name not in seen: + seen.append(name) + return _order(seen, ["Short", "Web"]) + + +def _throughput_table(data, dist, preferred, relabel): + # THROUGHPUT table for one distribution: upper-bound-per-row, algorithm-per- + # column, GiB/s; bold marks the fastest (highest) at each bound. Keys are + # ":"; rows are that distribution's bounds ascending. + algos = _order(list(data), preferred) + prefix = f"{dist}:" + bounds = sorted({int(k[len(prefix):]) for a in algos for k in data.get(a, {}) if k.startswith(prefix)}) + headers = ["max len"] + [relabel.get(a, a) for a in algos] + aligns = ["r"] + ["r"] * len(algos) rows = [] - for size in sizes: - cells = [_size_label(size)] + for bound in bounds: + key = f"{dist}:{bound}" + row = {a: _val(data[a][key]) for a in algos if key in data.get(a, {})} + best = max(row.values()) if row else None + cells = [_size_label(bound)] for algo in algos: - cell = data[algo].get(str(size)) - text = "-" if cell is None else _fmt(_val(cell)) - if cell is not None and abs(_val(cell) - mins[size]) < 1e-9: + if algo not in row: + cells.append("-") + continue + text = _fmt_gibs(row[algo]) + if best is not None and abs(row[algo] - best) < 1e-9: text = f"**{text}**" cells.append(text) rows.append(cells) return _md_table(headers, rows, aligns) -def render_tables(results): - ctx = results.get("context", {}) +def _agg_label(ctx): meas = ctx.get("measurement", {}) - agg = f"mean of the {meas.get('best_k', '?')} fastest of {meas.get('reps', '?')} reps" + return f"mean of the {meas.get('best_k', '?')} best of {meas.get('reps', '?')} reps" + + +def _readme_sizes(ctx): raw = ctx.get("readme_sizes") # curated subset emitted by the benchmark (kReadmeSizes) - sizes = [int(x) for x in str(raw).split(",") if x.strip().isdigit()] if raw else None - out = [ - f"", - "", - f"#### 64-bit one-shot throughput (ns/op, {agg}; lower is better)", - "", - _throughput_table(results["throughput64"], _ORDER_64, {}, sizes), - "", - f"#### 128-bit one-shot throughput (ns/op, {agg}; native-128 algorithms only)", - "", - _throughput_table(results["throughput128"], _ORDER_128, _LABEL_128, sizes), - "", - f"#### Mixed-length latency (ns/hash, {agg}; lower is better)", - "", - _latency_table(results["latency"], sizes), - ] - return "\n".join(out) + return [int(x) for x in str(raw).split(",") if x.strip().isdigit()] if raw else None + + +def _dist_chart_data(data, dist): + """One distribution's throughput as chart data: {algo: {bound: {"best": GiB/s}}}.""" + prefix = f"{dist}:" + out = {} + for algo, cases in data.items(): + for key, cell in cases.items(): + if key.startswith(prefix): + out.setdefault(algo, {})[key[len(prefix):]] = {"best": _val(cell) / (1024.0**3)} + return out + + +def _sections(results): + """Per-machine sections in layout order - 64-bit latency, 64-bit throughput + (Short, Web), 128-bit latency, 128-bit throughput (Short, Web) - each present + only when the dataset has that data. Each section is a dict with `tag`, + `title`, `heading`, `table`, `chart` (data or None), `order`, `relabel`, + `kind`; charts and tables are generated from the same list so they stay in + lockstep.""" + ctx = results.get("context", {}) + agg = _agg_label(ctx) + sizes = _readme_sizes(ctx) + out = [] + + def latency(tag, width, order, relabel, extra=""): + return { + "tag": tag, "title": f"{width}-bit latency", "kind": "latency", + "heading": f"{width}-bit latency (ns/hash at exact length, {agg}; {extra}lower is better)", + "table": _length_table(results[tag], order, relabel, sizes), + "chart": results[tag], "order": order, "relabel": relabel, + } + + def throughput(bucket, dist, width, order, relabel): + return { + "tag": f"{bucket}_{dist}", "title": f"{width}-bit throughput ({dist})", "kind": "throughput", + "heading": f"{width}-bit throughput, {dist} lengths (GiB/s over lengths <= max, {agg}; higher is better)", + "table": _throughput_table(results[bucket], dist, order, relabel), + "chart": _dist_chart_data(results[bucket], dist), "order": order, "relabel": relabel, + } + + if results.get("latency64"): + out.append(latency("latency64", 64, _ORDER_64, {})) + for dist in _dists(results.get("throughput64", {})): + out.append(throughput("throughput64", dist, 64, _ORDER_64, {})) + if results.get("latency128"): + out.append(latency("latency128", 128, _ORDER_128, _LABEL_128, extra="native-128 only; ")) + for dist in _dists(results.get("throughput128", {})): + out.append(throughput("throughput128", dist, 128, _ORDER_128, _LABEL_128)) + return out + + +def render_tables(results): + ctx = results.get("context", {}) + parts = [f""] + for section in _sections(results): + parts += ["", f"#### {section['heading']}", "", section["table"]] + return "\n".join(parts) # ---- compare: A (baseline) vs B (new) -------------------------------------- @@ -642,20 +729,30 @@ def _compare_bucket(base_data, new_data, preferred, relabel): def render_compare(base, new): """Markdown Δ% report between two canonical datasets. Shows both machine labels (comparing across machines is legitimate - it is the reader's call), - then a per-length Δ% table with a geomean row for each bucket.""" + then a per-case Δ% table with a geomean row for each section.""" out = [ - "Δ% = (B - A) / A per case; negative = B faster (lower ns is better).", + "Δ% = (B - A) / A per case. Latency: negative = B faster (lower ns). " + "Throughput: positive = B faster (higher bytes/s).", f"- A: {_machine_label(base.get('context', {}))}", f"- B: {_machine_label(new.get('context', {}))}", ] for key, order, relabel, title in ( - ("throughput64", _ORDER_64, {}, "64-bit one-shot throughput"), - ("throughput128", _ORDER_128, _LABEL_128, "128-bit one-shot throughput"), - ("latency", _ORDER_64, {}, "Mixed-length latency"), + ("latency64", _ORDER_64, {}, "64-bit latency"), + ("latency128", _ORDER_128, _LABEL_128, "128-bit latency"), ): table = _compare_bucket(base.get(key) or {}, new.get(key) or {}, order, relabel) if table: out += ["", f"#### {title} (Δ%)", "", table] + for key, order, relabel, width in ( + ("throughput64", _ORDER_64, {}, "64-bit"), + ("throughput128", _ORDER_128, _LABEL_128, "128-bit"), + ): + for dist in _dists(base.get(key) or {}): + table = _compare_bucket( + _dist_chart_data(base.get(key) or {}, dist), _dist_chart_data(new.get(key) or {}, dist), order, relabel + ) + if table: + out += ["", f"#### {width} throughput, {dist} (Δ%)", "", table] return "\n".join(out) @@ -676,8 +773,8 @@ def _linear_ticks(vmax, target=6): return ticks -def _svg_plot(data, algos, title, path, label_map=None, subtitle=None, linear_y=False): - """Dependency-free ns-vs-length SVG, one line per algorithm (best-k mean). +def _svg_plot(data, algos, title, path, label_map=None, subtitle=None, linear_y=False, y_label="ns / op", x_label="key length"): + """Dependency-free value-vs-length SVG, one line per algorithm (best-k mean). The x-axis (key length) is ALWAYS log-scaled: lengths are sampled geometrically (1..4096), so a linear x would bunch every small key at the @@ -747,11 +844,11 @@ def py(ns): if int(round(math.log2(size))) % 2 == 0: svg.append(f'{_size_label(size)}') size *= 2 - svg.append(f'key length (log scale)') + svg.append(f'{x_label} (log scale)') svg.append( f'' - f'ns / op ({"linear" if linear_y else "log"} scale)' + f'{y_label} ({"linear" if linear_y else "log"} scale)' ) for i, algo in enumerate(algos): # one polyline + legend row per algorithm color = colors[i % len(colors)] @@ -867,11 +964,24 @@ def _extract_bundle(path, dest): def _results_from_bundle(path): - """Load the canonical results.json out of a data bundle .tgz (the same - payload `bundle` packs), so table/plot commands accept a bundle directly.""" + """Canonical results from a data bundle .tgz. The raw google/benchmark JSON is + the source of truth, so we RE-DISTILL it with the current tool - a bundle + measured before the latency/throughput split still yields today's schema (e.g. + latency tables from old data) with no re-packing. The bundle's own + machine/source context is kept (from its stored results.json); we fall back to + the stored results.json only if the bundle carries no raw.""" with tempfile.TemporaryDirectory() as tmp: _extract_bundle(path, tmp) - return _load_json(os.path.join(tmp, "results.json")) + stored_path = os.path.join(tmp, "results.json") + stored = _load_json(stored_path) if os.path.exists(stored_path) else {} + raws = sorted(f for f in os.listdir(tmp) if "raw" in f and ".json" in f) + if not raws: + return stored + buckets, reps, best_k = _distill_buckets(_load_json(os.path.join(tmp, raws[-1]))) + ctx = dict(stored.get("context") or {}) + ctx.setdefault("measurement", {}) + ctx["measurement"] = {**ctx["measurement"], "reps": reps, "best_k": best_k} + return {"context": ctx, **buckets} def _load_dataset(path): @@ -942,20 +1052,20 @@ def _fill_missing_measured(measured): def _render_charts(full, stem, charts_dir, subtitle): - """Write the 64/128 labeled SVGs for one machine; return [(tag, filename), ...].""" + """Write one labeled SVG per section that has chart data (latency = ns-vs-length, + throughput = GiB/s-vs-upper-bound); return [(tag, filename), ...] in layout order.""" written = [] - for key, order, tag, labels in ( - ("throughput64", _ORDER_64, "64", None), - ("throughput128", _ORDER_128, "128", _LABEL_128), - ): - if not full.get(key): + for section in _sections(full): + data = section["chart"] + if not data: continue - name = f"{stem}_{tag}.svg" + name = f"{stem}_{section['tag']}.svg" + y_label, x_label = ("GiB / s", "max length") if section["kind"] == "throughput" else ("ns / op", "key length") _svg_plot( - full[key], _order(list(full[key]), order), f"mbo/hash - {tag}-bit one-shot throughput", - os.path.join(charts_dir, name), labels, subtitle=subtitle, + data, _order(list(data), section["order"]), f"mbo/hash - {section['title']}", + os.path.join(charts_dir, name), section["relabel"], subtitle=subtitle, y_label=y_label, x_label=x_label, ) - written.append((tag, name)) + written.append((section["tag"], name)) return written @@ -1111,21 +1221,18 @@ def main(argv): if args.command == "plot": results = _resolve_dataset(args) base, ext = os.path.splitext(_timestamped(args.out, stamp)) - # (bucket key, algorithm order, title, filename suffix, display relabel). - # Latency now sweeps the full size set (kFullSizes), so it renders as a - # log-log curve like throughput. `--kind` picks which curves to write. - throughput = [ - ("throughput64", _ORDER_64, "64-bit one-shot throughput", "_64", None), - ("throughput128", _ORDER_128, "128-bit one-shot throughput", "_128", _LABEL_128), - ] - latency = [("latency", _ORDER_64, "mixed-length latency", "_latency", None)] - selected = {"throughput": throughput, "latency": latency, "all": throughput + latency}[args.kind] linear_y = args.scale == "linear-log" - for key, order, title, suffix, labels in selected: - data = results.get(key) - if data: # _svg_plot also no-ops on empty, but skip cleanly (e.g. no 128-bit data) - path = f"{base}{suffix}{ext}" - _svg_plot(data, _order(list(data), order), f"mbo/hash - {title}", path, labels, linear_y=linear_y) + # `--kind` picks latency (ns-vs-length) and/or throughput (GiB/s-vs-bound) + # sections; one SVG per section, suffixed by its tag. + for section in _sections(results): + data = section["chart"] + if not data or (args.kind != "all" and section["kind"] != args.kind): + continue + y_label, x_label = ("GiB / s", "max length") if section["kind"] == "throughput" else ("ns / op", "key length") + _svg_plot( + data, _order(list(data), section["order"]), f"mbo/hash - {section['title']}", + f"{base}_{section['tag']}{ext}", section["relabel"], linear_y=linear_y, y_label=y_label, x_label=x_label, + ) return 0 if args.command == "smhasher": @@ -1180,19 +1287,19 @@ def main(argv): rel = os.path.relpath(args.charts_dir, os.path.dirname(os.path.abspath(args.readme))) blocks = [] # (label, bundle_path, section) - sorted by label below, not by input order for bundle_path in args.bundles: - with tempfile.TemporaryDirectory() as tmp: - _extract_bundle(bundle_path, tmp) - full = _load_json(os.path.join(tmp, "results.json")) - ctx = full.get("context", {}) - label = _machine_label(ctx) - embeds = [ - f"![mbo/hash {tag}-bit throughput vs key length, log-log]({rel}/{name})" - for tag, name in _render_charts(full, _bundle_stem(ctx), args.charts_dir, label) - ] - # `### {label}` heads the whole block (machine · compiler · sha), covering - # both the charts (which also carry it as a subtitle) and the tables. - blocks.append((label, bundle_path, "\n".join([f"### {label}", "", *embeds, "", render_tables(full)]))) - print(f"published {label}", file=sys.stderr) + full = _results_from_bundle(bundle_path) # re-distilled from the bundle's raw (see B) + ctx = full.get("context", {}) + label = _machine_label(ctx) + charts = dict(_render_charts(full, _bundle_stem(ctx), args.charts_dir, label)) # tag -> filename + # `### {label}` heads the block; each section is its chart (if any) + # immediately followed by its table, in layout order. + parts = [f"### {label}", "", f""] + for section in _sections(full): + if section["tag"] in charts: + parts += ["", f"![mbo/hash {section['title']}, {label}]({rel}/{charts[section['tag']]})"] + parts += ["", f"#### {section['heading']}", "", section["table"]] + blocks.append((label, bundle_path, "\n".join(parts))) + print(f"published {label}", file=sys.stderr) # Order sections (and the manifest) by the generated header, so the README # is stable regardless of the order bundles were passed on the command line. blocks.sort(key=lambda block: block[0]) @@ -1214,10 +1321,10 @@ def main(argv): bundles = match.group(1).split() mismatches = [] for bundle_path in bundles: + full = _results_from_bundle(bundle_path) # re-distilled from the bundle's raw (see B) + ctx = full.get("context", {}) with tempfile.TemporaryDirectory() as tmp: - _extract_bundle(bundle_path, tmp) - full = _load_json(os.path.join(tmp, "results.json")) - for _, name in _render_charts(full, _bundle_stem(full.get("context", {})), tmp, _machine_label(full.get("context", {}))): + for _, name in _render_charts(full, _bundle_stem(ctx), tmp, _machine_label(ctx)): committed = os.path.join(args.charts_dir, name) if not os.path.exists(committed) or not filecmp.cmp(os.path.join(tmp, name), committed, shallow=False): mismatches.append(name) diff --git a/mbo/hash/measurements/hash_throughput_128.svg b/mbo/hash/measurements/hash_throughput_128.svg deleted file mode 100644 index 8116a4b..0000000 --- a/mbo/hash/measurements/hash_throughput_128.svg +++ /dev/null @@ -1,39 +0,0 @@ - - -mbo/hash - 128-bit one-shot throughput - -10 - -100 - -1B - - -4B - - -16B - - -64B - - -256B - - -1Ki - - -4Ki -key length (log scale) -ns / op (log scale) - - -jumbo - - -xxh3 - - -murmur3 - diff --git a/mbo/hash/measurements/hash_throughput_64.svg b/mbo/hash/measurements/hash_throughput_64.svg deleted file mode 100644 index 2d30614..0000000 --- a/mbo/hash/measurements/hash_throughput_64.svg +++ /dev/null @@ -1,58 +0,0 @@ - - -mbo/hash - 64-bit one-shot throughput - -1 - -10 - -100 - -1000 - -1B - - -4B - - -16B - - -64B - - -256B - - -1Ki - - -4Ki -key length (log scale) -ns / op (log scale) - - -mumbo - - -rapidhash - - -xxh3 - - -xxh64 - - -murmur3 - - -siphash24 - - -fnv1a - - -dumbo - From 25b623748bebd51f6599ecc8e57290872d51d91d Mon Sep 17 00:00:00 2001 From: helly25 <6420169+helly25@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:04:59 +0100 Subject: [PATCH 2/3] feat(hash): 1-2-5 y-axis ticks on log charts (throughput readability) The narrow ~1-decade GiB/s throughput axis only labelled decade powers (1, 10); add 1-2-5 x 10^n gridlines/labels per decade (also denser on the latency axis). Regenerated the committed charts. --- ...50x-16-core-processor_gcc-15_latency128.svg | 10 ++++++++++ ...950x-16-core-processor_gcc-15_latency64.svg | 16 ++++++++++++++++ ...ore-processor_gcc-15_throughput64_Short.svg | 4 ++++ ...-core-processor_gcc-15_throughput64_Web.svg | 6 ++++++ ...-arm64-apple-m5-pro_clang-21_latency128.svg | 10 ++++++++++ ...s-arm64-apple-m5-pro_clang-21_latency64.svg | 18 ++++++++++++++++++ ...pple-m5-pro_clang-21_throughput64_Short.svg | 2 ++ ...-apple-m5-pro_clang-21_throughput64_Web.svg | 4 ++++ mbo/hash/measurements/hash_benchmark_report.py | 14 ++++++++------ 9 files changed, 78 insertions(+), 6 deletions(-) diff --git a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency128.svg b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency128.svg index 47d4ab0..ec5b686 100644 --- a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency128.svg +++ b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency128.svg @@ -2,10 +2,20 @@ mbo/hash - 128-bit latency AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8 + +2 + +5 10 + +20 + +50 100 + +200 1 B diff --git a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency64.svg b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency64.svg index acb5744..4cb8fa6 100644 --- a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency64.svg +++ b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_latency64.svg @@ -2,14 +2,30 @@ mbo/hash - 64-bit latency AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8 + +0.5 1 + +2 + +5 10 + +20 + +50 100 + +200 + +500 1000 + +2000 1 B diff --git a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Short.svg b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Short.svg index 912dc3c..472948e 100644 --- a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Short.svg +++ b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Short.svg @@ -4,6 +4,10 @@ AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8 1 + +2 + +5 10 diff --git a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Web.svg b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Web.svg index 3d68e6b..4019f59 100644 --- a/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Web.svg +++ b/mbo/hash/measurements/charts/linux-x86-64-amd-ryzen-9-9950x-16-core-processor_gcc-15_throughput64_Web.svg @@ -2,8 +2,14 @@ mbo/hash - 64-bit throughput (Web) AMD Ryzen 9 9950X 16-Core Processor · Linux · x86_64 · 10-core · gcc-15 · 80a4c8a8 + +2 + +5 10 + +20 16 B diff --git a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency128.svg b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency128.svg index 4fce194..69db5c3 100644 --- a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency128.svg +++ b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency128.svg @@ -2,10 +2,20 @@ mbo/hash - 128-bit latency Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8 + +5 10 + +20 + +50 100 + +200 + +500 1 B diff --git a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency64.svg b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency64.svg index 1be820c..c2783c3 100644 --- a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency64.svg +++ b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_latency64.svg @@ -2,14 +2,32 @@ mbo/hash - 64-bit latency Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8 + +0.5 1 + +2 + +5 10 + +20 + +50 100 + +200 + +500 1000 + +2000 + +5000 1 B diff --git a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Short.svg b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Short.svg index 08e3604..a13d52e 100644 --- a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Short.svg +++ b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Short.svg @@ -4,6 +4,8 @@ Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8 1 + +2 16 B diff --git a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Web.svg b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Web.svg index 1b305e3..81b9a64 100644 --- a/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Web.svg +++ b/mbo/hash/measurements/charts/macos-arm64-apple-m5-pro_clang-21_throughput64_Web.svg @@ -4,6 +4,10 @@ Apple M5 Pro · MacOS · arm64 · 18-core · clang-21 · 80a4c8a8 1 + +2 + +5 10 diff --git a/mbo/hash/measurements/hash_benchmark_report.py b/mbo/hash/measurements/hash_benchmark_report.py index 0544be1..494c963 100755 --- a/mbo/hash/measurements/hash_benchmark_report.py +++ b/mbo/hash/measurements/hash_benchmark_report.py @@ -827,14 +827,16 @@ def py(ns): y = py(tick) svg.append(f'') svg.append(f'{tick:g}') - else: # y gridlines + labels at each power of ten in range + else: # y gridlines + labels at 1-2-5 x 10^exp within range (readable on both + # the ~4-decade latency axis and the narrow ~1-decade throughput axis) exp = math.floor(ly0) while exp <= math.ceil(ly1): - ns = 10.0**exp - if ly0 - 1e-9 <= math.log10(ns) <= ly1 + 1e-9: - y = py(ns) - svg.append(f'') - svg.append(f'{ns:g}') + for mantissa in (1, 2, 5): + val = mantissa * 10.0**exp + if ly0 - 1e-9 <= math.log10(val) <= ly1 + 1e-9: + y = py(val) + svg.append(f'') + svg.append(f'{val:g}') exp += 1 size = 1 # x gridlines at powers of two, labels at powers of four while size <= sizes[-1]: From 2bee2dfb7412909328c525e7281b5f49c2f5dc49 Mon Sep 17 00:00:00 2001 From: helly25 <6420169+helly25@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:13:31 +0100 Subject: [PATCH 3/3] docs(hash): frame mumbo as the all-round default (not just latency) The headline claimed mumbo/jumbo "post the best mixed-length latency"; the new data does not cleanly support a raw-latency win. Reframe mumbo as the all-round default - among the fastest on every machine, SMHasher3-clean, notice-free Apache-2.0, with streaming and a Starlark port - and jumbo as the excellent native 128-bit choice. --- mbo/hash/README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mbo/hash/README.md b/mbo/hash/README.md index b8f17a8..c66c58d 100644 --- a/mbo/hash/README.md +++ b/mbo/hash/README.md @@ -3,11 +3,13 @@ Fast, constexpr-safe, non-cryptographic hashing, built around the in-house **mumbo/jumbo and dumbo** family: notice-free, pure Apache-2.0, and MUM-based (widening multiply). All three pass [SMHasher3](https://gitlab.com/fwojcik/smhasher3) -clean (188/188): `mumbo` (64-bit) and its native 128-bit sibling `jumbo` (the -only clean native 128 we measured) post the best mixed-length latency in our -benchmarks, and `dumbo` is a compact single-lane companion with a very different -profile - fastest here on tiny keys, slower on bulk - that trades reach for size, -not quality. +clean (188/188). `mumbo` (64-bit) is the all-round default: among the fastest +hashes here on every machine we measured, SMHasher3-clean, notice-free Apache-2.0, +and with both streaming and a Starlark port - few alternatives combine all of +these. Its native 128-bit sibling `jumbo` is the only clean native 128 we +measured and an excellent 128-bit choice. `dumbo` is a compact single-lane +companion with a very different profile - fastest here on tiny keys, slower on +bulk - that trades reach for size, not quality. It also ships a **build-seed mangle** (`hash_mangle.h`): restricted/limited, constexpr-safe compile-time hash mangling with release-time rotation enforcement.