Skip to content

feat(hash): add canonical FNV-1a, XXH64, and MurmurHash3 implementations - #209

Merged
helly25 merged 3 commits into
mainfrom
hash/more-algos
Jul 2, 2026
Merged

feat(hash): add canonical FNV-1a, XXH64, and MurmurHash3 implementations#209
helly25 merged 3 commits into
mainfrom
hash/more-algos

Conversation

@helly25

@helly25 helly25 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #208: more hash algorithms, plugged into the templated test/benchmark framework.

What

Three well-known algorithms as constexpr-safe, dependency-free headers, each in its own namespace and usable via GetHash<&Fn>:

  • mbo::hash::fnv1a::GetHash64 — FNV-1a 64 (public domain)
  • mbo::hash::xxh64::GetHash64 — XXH64 / xxHash 64-bit (BSD-2 spec)
  • mbo::hash::murmur3::GetHash64/GetHash128 — MurmurHash3 x64 128 (public domain); the second 128-bit-based algorithm, exercising the framework's width detection

All produce the published reference values on every platform — the algorithms are little-endian defined and the loads use the endian-independent hash_internal helpers (new Load32 for XXH64's tail).

mbo::hash::Hash128To64 is now public API, with the fold recipe documented at the alias: Hash128To64(murmur3::GetHash128(data)) gives a fold-mixed (non-canonical) 64-bit murmur3; murmur3::GetHash64 stays the canonical h1 truncation.

Testing

  • Known-answer tests: vectors generated with the reference implementations (python xxhash / mmh3, FNV per spec), covering each algorithm's block/tail boundaries and non-zero seeds. These caught a wrong Murmur3 constant during development — the vectors are doing their job.
  • Single algorithm list: algo::AllAlgorithms in hash_test_util.h is the single source of truth — the typed test suite and the benchmark both derive from it, so one added descriptor is tested and benchmarked automatically (128-bit benchmarks auto-register where HasHash128 detects them).
  • 47 tests: 44 pass, 3 skips = exactly the 64-bit-only algorithms skipping the 128-bit test.

Benchmark

Apple M-series, bazel run -c opt //mbo/hash:hash_benchmark -- --benchmark_min_time=0.2s. Throughput in GiB/s (higher is better), random data per length.

64-bit (GetHash64)

len simple mh fnv1a xxh64 murmur3
1 B 1.15 0.54 1.91 0.52 0.31
4 B 3.85 1.43 2.58 1.87 0.84
16 B 6.87 9.75 2.25 4.90 3.81
64 B 6.06 7.11 1.45 8.75 6.62
256 B 3.31 6.27 0.83 14.68 7.07
1 KB 2.45 4.70 0.70 17.17 5.75
4 KB 2.20 4.36 0.68 16.73 5.36

128-bit (GetHash128)

len mh murmur3
1 B 0.42 0.30
16 B 5.21 3.80
256 B 6.52 7.07
4 KB 4.44 5.38

Takeaways: mh is the best all-rounder at typical key sizes (≈9.8 GiB/s @ 16 B); xxh64's 4-accumulator stripes dominate from ~64 B up (≈17 GiB/s peak) — a possible future direction for mh's block loop; fnv1a only wins at ~1 byte and falls off hard; murmur3 is a solid mid-fielder and slightly beats mh on large 128-bit hashing.

helly25 added 3 commits July 2, 2026 23:10
Adds three well-known hash algorithms as constexpr-safe, dependency-free
headers, each in its own namespace and pluggable via GetHash<&Fn>:

- mbo::hash::fnv1a::GetHash64   -- FNV-1a 64 (public domain)
- mbo::hash::xxh64::GetHash64   -- XXH64 / xxHash 64-bit (BSD-2 spec)
- mbo::hash::murmur3::GetHash64/GetHash128 -- MurmurHash3 x64 128 (public domain)

All three produce the published reference values on every platform (the
algorithms are little-endian defined; loads use the endian-independent
hash_internal helpers, with a new Load32 for XXH64's tail). Known-answer
tests pin values generated with the reference implementations (python
xxhash / mmh3) across each algorithm's block/tail boundaries and seeds.

The algorithms register as descriptors in hash_test_util.h, so the typed
framework covers them automatically (constexpr==runtime, collisions,
avalanche, 64/128-bit detection -- murmur3 is the second 128-bit-based
algorithm). The typed Hash128 test no longer assumes the mh fold relation;
Get64/Get128 relations are per-algorithm tests (mh folds, murmur3
truncates to h1). Benchmark now compares all five algorithms.
… recipe

Promote hash_internal::Hash128To64 into mbo::hash so composing a fold-mixed
64-bit value from any 128-bit based algorithm is official API, with the recipe
documented at the alias:

  uint64_t hash = mbo::hash::Hash128To64(mbo::hash::murmur3::GetHash128(data));

murmur3::GetHash64 stays the canonical h1 truncation (ecosystem contract);
the fold is the user-composable alternative. Adds a test pinning the recipe.
The typed test suite and the benchmark each maintained their own algorithm
registrations, so a new descriptor could be added to one and forgotten in the
other. Introduce algo::AllAlgorithms (a tuple of all descriptors) in
hash_test_util.h as the single source of truth:

- hash_test.cc converts the tuple into the gtest type list.
- hash_benchmark.cc registers benchmarks by folding over the tuple, adding the
  128-bit benchmark automatically where HasHash128 detects one (custom main
  with benchmark::RegisterBenchmark replaces the per-algorithm macros).

Adding a descriptor to AllAlgorithms now tests AND benchmarks it.
@helly25
helly25 requested a review from Fab-Cat July 2, 2026 22:29
@helly25
helly25 enabled auto-merge (squash) July 2, 2026 22:33
@helly25
helly25 merged commit d935ab7 into main Jul 2, 2026
38 checks passed
@helly25
helly25 deleted the hash/more-algos branch July 2, 2026 23:53
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