Skip to content

feat(hash): faster constexpr hash (mbo::hash::mh) + hardening & tests - #208

Merged
helly25 merged 5 commits into
mainfrom
hash/harden-and-test
Jul 2, 2026
Merged

feat(hash): faster constexpr hash (mbo::hash::mh) + hardening & tests#208
helly25 merged 5 commits into
mainfrom
hash/harden-and-test

Conversation

@helly25

@helly25 helly25 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Hardens and tests the new hash work, and makes it the default.

What

  • New mbo::hash::GetHash64 / GetHash128 + Hash128 type (namespace mbo::hash::mh), now the default behind mbo::hash::GetHash*. mbo::hash::simple::GetHash is deprecated (kept as simple::GetHash64).
  • Module bumped to 0.13.0 (matches the CHANGELOG entry).

Implementation

  • Dual-lane rotate-multiply core with full-width odd multipliers + MurmurHash3 fmix64 finalize. Strong avalanche (verified ~0.50), ~2× simple's throughput on ≥1 KB.
  • Endian-independent little-endian byte loads (portable values across platforms; replaces bit_cast).
  • Small-input (≤32 B) fast path for GetHash64: single lane + one finalize so short keys don't pay the 128-bit fold overhead (16 B: 9.8 GiB/s vs simple 6.8; 1 B improved 2.9→1.7 ns).
  • Hash128 gets dependency-free AbslHashValue / AbslStringify (no abseil dep added to hash_cc).

Testing

  • Templated framework (hash_test_util.h): algorithm descriptors + a HasHash128/kHashBits trait that detects 64- vs 128-bit algorithms. hash_test.cc runs TYPED_TESTs over {simple, mh} — constexpr==runtime, empty/null, distinctness, collisions, and avalanche on both code paths. The 128-bit test auto-skips for the 64-bit-only simple.
  • hash_benchmark (google_benchmark, tags=["manual"]): length-bucketed throughput to substantiate the perf claims.

Fixes rolled in (from the review)

Missing <bit>/<cstddef>/<string_view> includes, malformed NOLINT, IWYU private pragmas on impl headers, accurate GetHash doc + stability note (values are not stable across versions and not for persistence/crypto), and CHANGELOG/README reconciliation ("deprecated", not "removed").

Perf note (reviewer FYI)

For 1–4 byte keys simple is still marginally faster (inherent fmix64 finalize cost); mh wins or ties at typical key sizes (≥16 B) and is ~2× faster on large inputs. Hash values are intentionally not stable across versions.

helly25 added 5 commits July 2, 2026 20:32
Add mbo::hash::GetHash64 / GetHash128 and the Hash128 type (namespace
mbo::hash::mh), now the default behind mbo::hash::GetHash*, and deprecate
mbo::hash::simple::GetHash.

Implementation:
- Dual-lane rotate-multiply core with full-width odd multipliers and a
  MurmurHash3 fmix64 finalize (strong avalanche; ~2x simple's throughput on
  >=1KB inputs).
- Endian-independent little-endian byte loads (portable across platforms).
- Small-input (<=32B) fast path for GetHash64: single lane + one finalize so
  short keys don't pay the 128-bit fold overhead.
- Hash128 gains dependency-free AbslHashValue / AbslStringify.

Testing:
- Templated test/benchmark framework (hash_test_util.h) that drives multiple
  algorithms and detects 64- vs 128-bit ones; covers constexpr==runtime,
  empty/null, distinctness, collisions, and avalanche on both code paths.
- google_benchmark target (length-bucketed) to substantiate throughput.

Also: include/NOLINT/IWYU cleanups, accurate GetHash docs + stability note
(values not stable across versions / not for persistence), CHANGELOG + README,
and bump module to 0.13.0.
Deriving the mangle seed straight from __DATE__/__TIME__ produced a full 64-bit,
effectively unbounded seed that changed every compile, baking a different
constant into every build and defeating reproducible builds / build caches.

Now the date/time hash only selects a bucket via `% kMangleSeedCount` (default
16), and the bucket is expanded into a full-width constant so the whole hash is
still mangled. This bounds the number of possible outputs while keeping some
per-build variation; a hermetic build (pinned macros) collapses to one bucket.

Adds tests for the seed-independent mangle properties.
GetHash was hard-wired to the default 64-bit hash. Make it a defaulted
template `GetHash<Hash64Fn = &mh::GetHash64>(data, seed)` so the same
build-seed mangle can wrap any implementation matching
`Hash64Fn = uint64_t(*)(std::string_view, uint64_t) noexcept`.

Because default arguments don't apply through a function pointer, the pluggable
contract takes the seed explicitly and GetHash forwards it (default seed hoisted
to the shared `mh::kDefaultSeed`). Existing `GetHash(data)` calls are unchanged.
Adds a pluggability test.
Promote GetHash's seed from a runtime default argument to a second non-type
template parameter (default mh::kDefaultSeed), matching the hash-function
parameter. GetHash(data) is now a clean single-argument call; the seed is
customized at compile time via GetHash<&Fn, Seed>(data).
Adds an explicit, build-wide switch MBO_HASH_MANGLE (default 1). Defining it to
0 makes HashMangle the identity, so GetHash == GetHash64 -- fully reproducible
values for reproducible builds / golden tests.

Deliberately NOT tied to -O level or NDEBUG: HashMangle is inline constexpr, so
a per-config value would be an ODR violation and would make debug and release
builds disagree (a worse footgun than the mangle). A team wanting opt to disable
it sets -DMBO_HASH_MANGLE=0 in that config explicitly.

Adds hash_no_mangle_test (built with the flag off) so the disabled path stays
covered in CI.
@helly25
helly25 requested a review from Fab-Cat July 2, 2026 21:12
@helly25
helly25 merged commit c798caf into main Jul 2, 2026
38 checks passed
@helly25
helly25 deleted the hash/harden-and-test branch July 2, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants