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
1 change: 0 additions & 1 deletion .bazelignore
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
bazel-mbo
mbo/hash/measurements
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ jobs:
continue-on-error: ${{inputs.continue-on-error}}
steps:
- uses: actions/checkout@v6
- name: Fetch measurement bundles (Git LFS)
# The `//mbo/hash/measurements:quality_sh_test` bazel test reads the
# committed data bundles for the SMHasher3 quality tables; actions/checkout
# leaves them as LFS pointers, so materialize just those (each is small).
run: git lfs pull --include="mbo/hash/measurements/data/*.tgz"
- uses: bazelbuild/setup-bazelisk@v3
- name: Install GCC
if: ${{inputs.compiler == 'gcc' && inputs.gcc_version != ''}}
Expand Down
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ repos:
language: pygrep
entry: \s*#\s*include
files: MODULE.bazel
- id: no-deps-on-measurements
name: no deps on mbo/hash/measurements
description: |
mbo/hash/measurements is dev-only tooling, stripped from release archives
(release_prep.sh EXCLUDES). Nothing outside it may load from or depend on
it (label `//mbo/hash/measurements`), or a release build would carry a
dangling reference. Dependency edges stay one-way: measurements -> library.
language: pygrep
entry: //mbo/hash/measurements
files: \.(bazel|bzl)$
exclude: ^mbo/hash/measurements/
- id: github-bazelmod-patch-applies
name: github-bazelmod-patch-applies
description: |
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# 0.13.2

- Added the `quality` command to `hash_benchmark_report.py`: it generates the SMHasher3 "Results" table in `mbo/hash/README.md` from a curated source (verify with `--check`, source measured verdict/score from a fresh run with `--smhasher <bundle>`).
- Added the `quality` command to `hash_benchmark_report.py`: it generates both the "Algorithm overview" and SMHasher3 "Results" tables in `mbo/hash/README.md` from `hash_algorithms.json` (manual/editorial columns) merged with the measured verdict/score/failures re-parsed from a data bundle (`--bundle`); `--check` gates them, and a `consistency` command verifies all bundles from the same source SHA agree (SMHasher3 verdicts are machine-independent).
- Fixed SMHasher3 result parsing so an invalid hash name or crash reads as `ERROR` instead of a false `PASS`, and the pass/fail score and failing families now parse from the run Summary; legacy/short SMHasher3 names are aliased to their registered form when a measured dataset is loaded.
- Integrated `mbo/hash/measurements` as a normal dev package (dropped its nested module and `.bazelignore` entry; still stripped from release archives) and added a `quality_sh_test` bazel test gating the generated tables; the test CI job now fetches the measurement bundles (Git LFS) so it reads real data.
- Added a `no-deps-on-measurements` pre-commit guard so nothing outside the release-stripped `mbo/hash/measurements` may depend on it (which would dangle in releases).

# 0.13.1

Expand Down
8 changes: 8 additions & 0 deletions mbo/hash/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ load(":internal/hash_mangle_seed.bzl", "mangle_seed_gen")

package(default_visibility = ["//visibility:private"])

# Exported so the dev-only measurements quality test (under mbo/hash/measurements,
# stripped from release archives) can verify this README's generated SMHasher3
# table. Public visibility, so this line references no release-stripped package.
exports_files(
["README.md"],
visibility = ["//visibility:public"],
)

# Create custom bazel flag `--//mbo/hash:mangle_seed`: any printable-ASCII
# string (user name, release tag, date, ...) selecting the mangle constant.
# Folded to a bucket inside `mangle_seed_gen`, so caches converge (see
Expand Down
78 changes: 51 additions & 27 deletions mbo/hash/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

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). `mumbo` (64-bit) and its native 128-bit sibling `jumbo`
both pass [SMHasher3](https://gitlab.com/fwojcik/smhasher3) clean (188/188 -
`jumbo` is the only clean native 128 we measured) and post the best mixed-length
latency in our benchmarks; `dumbo` is the compact companion.
(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.

It also ships a **build-seed mangle** (`hash_mangle.h`): restricted/limited,
constexpr-safe compile-time hash mangling with release-time rotation enforcement.
Expand Down Expand Up @@ -70,17 +72,22 @@ Three entry points, split by contract:
This is the at-a-glance map; the `SMHasher3` column is a PASS/FAIL summary only.
For the exact score and the failing families see [Quality: SMHasher3](#quality-smhasher3).

| Algorithm | Widths | Available via | Starlark | NOTICE | Seeded | Streaming | SMHasher3 |
| ----------- | ------ | --------------------------------- | -------- | ----------------------- | ------ | --------- | --------- |
| `mumbo` | 64 | `hash.h` (default 64/32) | no | none (in-house) | yes | yes | PASS |
| `jumbo` | 128 | `hash.h` (default 128) | no | none (in-house) | yes | yes (64) | PASS |
| `murmur3` | 64/128 | `hash.h` | no | none (public domain) | yes | no | FAIL |
| `siphash` | 64 | `hash.h` | no | none (CC0) | keyed | yes | PASS |
| `fnv1a` | 64 | `hash.h` | yes | none (public domain) | yes | no | FAIL |
| `dumbo` | 64 | `hash.h` | yes | none (in-house) | yes | no | PASS |
| `rapidhash` | 64 | `hash_extra.h` + `:hash_extra_cc` | no | **MIT - ship NOTICE** | yes | no | PASS |
| `xxh64` | 64 | `hash_extra.h` + `:hash_extra_cc` | no | **BSD-2 - ship NOTICE** | yes | yes | FAIL |
| `xxh3` | 64/128 | `hash_extra.h` + `:hash_extra_cc` | no | **BSD-2 - ship NOTICE** | yes | no | FAIL |
<!-- BEGIN algorithm overview (generated by `hash_benchmark_report.py quality`; DO NOT EDIT) -->

| Algorithm | Bits | Available via | Starlark | NOTICE | Seeded | Streaming | SMHasher3 |
| ----------- | ---: | --------------------------------- | -------- | ----------------------- | ------ | --------- | --------- |
| `mumbo` | 64 | `hash.h` (default 64/32) | no | none (in-house) | yes | yes | PASS |
| `jumbo` | 128 | `hash.h` (default 128) | no | none (in-house) | yes | yes (64) | PASS |
| `murmur3` | 128 | `hash.h` | no | none (public domain) | yes | no | FAIL |
| `siphash` | 64 | `hash.h` | no | none (CC0) | keyed | yes | PASS |
| `fnv1a` | 64 | `hash.h` | yes | none (public domain) | yes | no | FAIL |
| `dumbo` | 64 | `hash.h` | yes | none (in-house) | yes | no | PASS |
| `rapidhash` | 64 | `hash_extra.h` + `:hash_extra_cc` | no | **MIT - ship NOTICE** | yes | no | PASS |
| `xxh64` | 64 | `hash_extra.h` + `:hash_extra_cc` | no | **BSD-2 - ship NOTICE** | yes | yes | FAIL |
| `xxh3` | 64 | `hash_extra.h` + `:hash_extra_cc` | no | **BSD-2 - ship NOTICE** | yes | no | FAIL |
| `xxh3` | 128 | `hash_extra.h` + `:hash_extra_cc` | no | **BSD-2 - ship NOTICE** | yes | no | FAIL |

<!-- END algorithm overview -->

Notes: the **Starlark** column marks the hashes also implemented at build time
in [`hash.bzl`](hash.bzl) (`hash.dumbo` and `hash.fnv1a`), kept byte-for-byte
Expand Down Expand Up @@ -430,18 +437,18 @@ numbers are directly comparable.

<!-- BEGIN SMHasher3 results (generated by `hash_benchmark_report.py quality`; DO NOT EDIT) -->

| 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] |
| `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-16], SeedBlockOffset [0-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 | 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] |

<!-- END SMHasher3 results -->

Expand Down Expand Up @@ -568,3 +575,20 @@ reads as the compact MUM hash rather than a second tuned one:
Full logs are not committed; regenerate as above. Last run (2026-07): all
three in-house hashes clean - `mumbo-64`/`jumbo-128` and `dumbo-64` PASS
188 / 188.
- To produce measurements on another machine (or refresh a machine's numbers),
run the tooling from the repo root (full per-machine and publish steps in
[`measurements/README.md`](measurements/README.md)):

```sh
# On the new machine: one perf sweep + the SMHasher3 battery, packed into a
# per-machine Git-LFS bundle whose path the script prints.
mbo/hash/measurements/run_measurements.py --config clang --jobs 4
git add mbo/hash/measurements/data/<bundle>.tgz
git lfs push origin HEAD && git push

# Re-render this README from the chosen bundles, then refresh the SMHasher3
# Results table from the same bundle's measured data.
mbo/hash/measurements/hash_benchmark_report.py publish --bundles data/<bundle>.tgz
mbo/hash/measurements/hash_benchmark_report.py quality --smhasher data/<bundle>.tgz
git add mbo/hash/README.md mbo/hash/measurements/charts
```
38 changes: 38 additions & 0 deletions mbo/hash/measurements/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: Copyright (c) The helly25 authors (helly25.com)
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Dev-only measurement tooling for mbo/hash (benchmark + SMHasher3 quality). The
# whole directory is stripped from release archives (release_prep.sh EXCLUDES), so
# nothing shipped depends on it; these targets exist only for the dev build/CI.

load("@helly25_bashtest//bashtest:bashtest.bzl", "bashtest")

package(default_visibility = ["//visibility:private"])

# Guards the generated SMHasher3 quality tables in //mbo/hash/README.md (overview
# + Results): `quality --check` (they match hash_algorithms.json x a measured
# bundle) and `consistency` (all bundles agree). Uses the system python3 (the tool
# has no third-party deps) and the Git-LFS bundles (materialized by test.yml / a
# local `git lfs pull`).
bashtest(
name = "quality_sh_test",
size = "small",
srcs = ["quality_sh_test.sh"],
data = [
"hash_algorithms.json",
"hash_benchmark_report.py",
"//mbo/hash:README.md",
] + glob(["data/*.tgz"]),
)
29 changes: 0 additions & 29 deletions mbo/hash/measurements/MODULE.bazel

This file was deleted.

Loading
Loading