From 4088c643f7d20c081ce0a781817475166663ec2c Mon Sep 17 00:00:00 2001 From: helly25 <6420169+helly25@users.noreply.github.com> Date: Sun, 19 Jul 2026 10:40:59 +0100 Subject: [PATCH 1/2] Updated `mumbo` to V5 which aims at improved Instruction-level parallelism (ILP). --- CHANGELOG.md | 1 + mbo/hash/README.md | 7 ++ mbo/hash/hash_internal_util.h | 138 ++++++++++++++++++++++++---------- mbo/hash/hash_mumbo.h | 124 +++++++++++++++--------------- 4 files changed, 173 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5a6498..ace88f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 0.13.3 - Added `diff-charts` sub-command for the `hash_benchmark_report.py` tool. +- Updated `mumbo` to V5 which aims at improved Instruction-level parallelism (ILP). # 0.13.2 diff --git a/mbo/hash/README.md b/mbo/hash/README.md index ecb9594..c018760 100644 --- a/mbo/hash/README.md +++ b/mbo/hash/README.md @@ -808,6 +808,13 @@ benchmark plus both SMHasher3 batteries): derive from secret pairs distinct from the 64-bit chain, so no lane ever equals the 64-bit hash. The table above reflects the latest completed batteries. +5. v5 (188/188 both widths, mbo version 0.13.3): Retains the exact mathematical + topology of v4 but restructures the runtime execution layer. The sequential + loops are unrolled and interleaved. This maximizes instruction-level + parallelism (ILP) and resolves hardware pipeline execution stalls without + altering the underlying hash values. Unfortunately even in 2026 the algorithm + uses more registers than compilers can use for full ILP, so either a redesign + or altogether new algorithm is needed. ### dumbo: the measured design iterations diff --git a/mbo/hash/hash_internal_util.h b/mbo/hash/hash_internal_util.h index 239a928..8b2cfbf 100644 --- a/mbo/hash/hash_internal_util.h +++ b/mbo/hash/hash_internal_util.h @@ -26,12 +26,20 @@ #include "mbo/hash/hash_types.h" +#if defined(__GNUC__) || defined(__clang__) +# define MBO_FORCE_INLINE inline __attribute__((always_inline)) +#elif defined(_MSC_VER) +# define MBO_FORCE_INLINE __forceinline +#else +# define MBO_FORCE_INLINE inline +#endif + namespace mbo::hash::hash_internal { -// NOLINTBEGIN(*-magic-numbers,*-pointer-arithmetic) +// NOLINTBEGIN(*-magic-numbers,*-pointer-arithmetic,*-identifier-naming) // Final mixer (MurmurHash3 `fmix64`) that spreads entropy across all 64 bits. -constexpr uint64_t Fmix64(uint64_t val) noexcept { +MBO_FORCE_INLINE constexpr uint64_t Fmix64(uint64_t val) noexcept { val ^= val >> 33U; val *= 0xff51afd7ed558ccdULL; val ^= val >> 33U; @@ -50,7 +58,7 @@ constexpr uint64_t Fmix64(uint64_t val) noexcept { // gcc 13: 8x movzx+or, ~3x slower hashing) -- hence the explicit `memcpy` path. // Both paths produce identical values; the ConstexprMatchesRuntime test guards // this equality. -constexpr uint64_t Load64(const char* ptr) noexcept { +MBO_FORCE_INLINE constexpr uint64_t Load64(const char* ptr) noexcept { if (!std::is_constant_evaluated()) { if constexpr (std::endian::native == std::endian::little) { uint64_t result = 0; @@ -70,7 +78,7 @@ constexpr uint64_t Load64(const char* ptr) noexcept { // unlike the hash algorithms in this library; the primitive lives here so the // dual-path byte-load logic exists exactly once. The runtime path is `memcpy` // plus a byteswap on little-endian targets (same rationale as `Load64`). -constexpr uint32_t Load32BE(const char* ptr) noexcept { +MBO_FORCE_INLINE constexpr uint32_t Load32BE(const char* ptr) noexcept { if (!std::is_constant_evaluated()) { #if defined(__GNUC__) || defined(__clang__) uint32_t result = 0; @@ -89,7 +97,7 @@ constexpr uint32_t Load32BE(const char* ptr) noexcept { // Loads 8 bytes as a **big-endian** `uint64_t` (same rationale as `Load32BE`; // used by the 64-bit-word digest specifications, e.g. SHA-512). -constexpr uint64_t Load64BE(const char* ptr) noexcept { +MBO_FORCE_INLINE constexpr uint64_t Load64BE(const char* ptr) noexcept { if (!std::is_constant_evaluated()) { #if defined(__GNUC__) || defined(__clang__) uint64_t result = 0; @@ -108,7 +116,7 @@ constexpr uint64_t Load64BE(const char* ptr) noexcept { } // Loads 4 bytes as a **little-endian** `uint32_t` (same rationale as `Load64`). -constexpr uint32_t Load32(const char* ptr) noexcept { +MBO_FORCE_INLINE constexpr uint32_t Load32(const char* ptr) noexcept { if (!std::is_constant_evaluated()) { if constexpr (std::endian::native == std::endian::little) { uint32_t result = 0; @@ -131,7 +139,7 @@ constexpr uint32_t Load32(const char* ptr) noexcept { // [n-4..n-1], and in the overlap both operands carry the same byte at the same // bit position, so the OR reproduces the exact little-endian value (the // canonical known-answer tests verify path equality). -constexpr uint64_t LoadTail(const char* ptr, std::size_t remaining) noexcept { +MBO_FORCE_INLINE constexpr uint64_t LoadTail(const char* ptr, std::size_t remaining) noexcept { if (!std::is_constant_evaluated()) { if constexpr (std::endian::native == std::endian::little) { if (remaining >= 4) { @@ -150,53 +158,107 @@ constexpr uint64_t LoadTail(const char* ptr, std::size_t remaining) noexcept { // Folds the two 64-bit lanes into one (offsetting `h2` so equal lanes do not // cancel) and finalizes the result. -constexpr uint64_t Hash128To64(Hash128 hash) noexcept { +MBO_FORCE_INLINE constexpr uint64_t Hash128To64(Hash128 hash) noexcept { const uint64_t combined = hash.h1 ^ (hash.h2 + 0x9e3779b97f4a7c15ULL); return Fmix64(combined); } +#if defined(__cpp_lib_int_128) +using mbo_uint128_t = std::uint128_t; +inline constexpr bool has_128_bit = true; +#elif defined(__SIZEOF_INT128__) +using mbo_uint128_t = unsigned __int128; +inline constexpr bool has_128_bit = true; +#else +// Fallback to 64-bit if compiling for a 32-bit system +using mbo_uint128_t = void; // Placeholder type, not usable +inline constexpr bool has_128_bit = false; +#endif + // Full 64x64->128 multiply; returns the low half in `h1` and the high half in // `h2`. Uses `__uint128_t` where the compiler provides it (gcc/clang, // constexpr-legal); the portable 32-bit schoolbook multiply otherwise. -constexpr Hash128 Mult128(uint64_t lhs, uint64_t rhs) noexcept { -#if defined(__SIZEOF_INT128__) - const auto product = static_cast(lhs) * static_cast(rhs); - return {.h1 = static_cast(product), .h2 = static_cast(product >> 64U)}; -#else // defined(__SIZEOF_INT128__) - const uint64_t lo_lo = (lhs & 0xFFFFFFFFULL) * (rhs & 0xFFFFFFFFULL); - const uint64_t hi_lo = (lhs >> 32U) * (rhs & 0xFFFFFFFFULL); - const uint64_t lo_hi = (lhs & 0xFFFFFFFFULL) * (rhs >> 32U); - const uint64_t hi_hi = (lhs >> 32U) * (rhs >> 32U); - const uint64_t cross = (lo_lo >> 32U) + (hi_lo & 0xFFFFFFFFULL) + lo_hi; - return { - .h1 = (cross << 32U) | (lo_lo & 0xFFFFFFFFULL), - .h2 = (hi_lo >> 32U) + (cross >> 32U) + hi_hi, - }; -#endif // defined(__SIZEOF_INT128__) +MBO_FORCE_INLINE constexpr Hash128 Mult128(uint64_t lhs, uint64_t rhs) noexcept { + if constexpr (has_128_bit) { + const auto product = static_cast(lhs) * static_cast(rhs); + return {.h1 = static_cast(product), .h2 = static_cast(product >> 64U)}; + } else { + const uint64_t lo_lo = (lhs & 0xFFFFFFFFULL) * (rhs & 0xFFFFFFFFULL); + const uint64_t hi_lo = (lhs >> 32U) * (rhs & 0xFFFFFFFFULL); + const uint64_t lo_hi = (lhs & 0xFFFFFFFFULL) * (rhs >> 32U); + const uint64_t hi_hi = (lhs >> 32U) * (rhs >> 32U); + const uint64_t cross = (lo_lo >> 32U) + (hi_lo & 0xFFFFFFFFULL) + lo_hi; + return { + .h1 = (cross << 32U) | (lo_lo & 0xFFFFFFFFULL), + .h2 = (hi_lo >> 32U) + (cross >> 32U) + hi_hi, + }; + } } // Full 64x64->128 multiply folded to 64 bits by XORing the halves (the core // mixer of the xxh3/wyhash algorithm family). Uses `__uint128_t` where the // compiler provides it (gcc/clang, constexpr-legal); the portable 32-bit // schoolbook multiply otherwise. -constexpr uint64_t Mul128Fold64(uint64_t lhs, uint64_t rhs) noexcept { -#if defined(__SIZEOF_INT128__) - const auto product = static_cast(lhs) * static_cast(rhs); - return static_cast(product) ^ static_cast(product >> 64U); -#else // defined(__SIZEOF_INT128__) - const uint64_t lo_lo = (lhs & 0xFFFFFFFFULL) * (rhs & 0xFFFFFFFFULL); - const uint64_t hi_lo = (lhs >> 32U) * (rhs & 0xFFFFFFFFULL); - const uint64_t lo_hi = (lhs & 0xFFFFFFFFULL) * (rhs >> 32U); - const uint64_t hi_hi = (lhs >> 32U) * (rhs >> 32U); - const uint64_t cross = (lo_lo >> 32U) + (hi_lo & 0xFFFFFFFFULL) + lo_hi; - const uint64_t upper = (hi_lo >> 32U) + (cross >> 32U) + hi_hi; - const uint64_t lower = (cross << 32U) | (lo_lo & 0xFFFFFFFFULL); - return lower ^ upper; -#endif // defined(__SIZEOF_INT128__) +MBO_FORCE_INLINE constexpr uint64_t Mul128Fold64(uint64_t lhs, uint64_t rhs) noexcept { + if constexpr (has_128_bit) { + const auto product = static_cast(lhs) * static_cast(rhs); + return static_cast(product) ^ static_cast(product >> 64U); + } else { + const uint64_t lo_lo = (lhs & 0xFFFFFFFFULL) * (rhs & 0xFFFFFFFFULL); + const uint64_t hi_lo = (lhs >> 32U) * (rhs & 0xFFFFFFFFULL); + const uint64_t lo_hi = (lhs & 0xFFFFFFFFULL) * (rhs >> 32U); + const uint64_t hi_hi = (lhs >> 32U) * (rhs >> 32U); + const uint64_t cross = (lo_lo >> 32U) + (hi_lo & 0xFFFFFFFFULL) + lo_hi; + const uint64_t upper = (hi_lo >> 32U) + (cross >> 32U) + hi_hi; + const uint64_t lower = (cross << 32U) | (lo_lo & 0xFFFFFFFFULL); + return lower ^ upper; + } } -// NOLINTEND(*-magic-numbers,*-pointer-arithmetic) +// Loads the (0..16 byte) small-key input into two words; see the structure +// notes in the header comment. +struct SmallInput { + uint64_t a = 0; + uint64_t b = 0; +}; + +MBO_FORCE_INLINE constexpr SmallInput LoadSmall(const char* ptr, std::size_t len) noexcept { + // NOLINTNEXTLINE(readability-identifier-length): byte-widening helper. + const auto u8 = [](char chr) constexpr { return static_cast(static_cast(chr)); }; + // If-ladder, common 4..16 range gated first: the dense switch compiled to a + // jump table (indirect branch + table load) that dominated the small-key + // cost; here 9..16 (the bulk of hashed string keys, and of the SSO range) + // resolve on the first compare. Values are byte-identical to the previous + // switch at every length. + if (len >= 4) { + if (len >= 9) { // 9..16: two 64-bit loads overlapping the end. + return {.a = Load64(ptr), .b = Load64(ptr + len - 8)}; + } + if (len == 8) { + const uint64_t val = Load64(ptr); + return {.a = val, .b = val}; + } + return {.a = Load32(ptr), .b = Load32(ptr + len - 4)}; // 4..7 (len 4 -> both equal) + } + if (len == 3) { + const uint64_t val = (u8(ptr[0]) << 45U) | (u8(ptr[1]) << 8U) | u8(ptr[2]); + return {.a = val, .b = val}; + } + if (len == 2) { + const uint64_t val = (u8(ptr[0]) << 45U) | (u8(ptr[1]) << 8U) | u8(ptr[0]); + return {.a = val, .b = val}; + } + if (len == 1) { + const uint64_t val = u8(ptr[0]); + return {.a = (val << 45U) | val, .b = (val << 45U) | val}; + } + return {.a = 0, .b = 0}; +} + +// NOLINTEND(*-magic-numbers,*-pointer-arithmetic,*-identifier-naming) } // namespace mbo::hash::hash_internal +#undef MBO_FORCE_INLINE + #endif // MBO_HASH_HASH_INTERNAL_UTIL_H_ diff --git a/mbo/hash/hash_mumbo.h b/mbo/hash/hash_mumbo.h index d179d1d..b250431 100644 --- a/mbo/hash/hash_mumbo.h +++ b/mbo/hash/hash_mumbo.h @@ -1,4 +1,3 @@ -// 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"); @@ -50,15 +49,16 @@ // - 17..127 bytes: one sequential MUM chain, 16 bytes per step; the final // <= 16 bytes are read as two loads overlapping the end (no tail loops). // - >= 128 bytes: eight independent MUM chains over a 128-byte fetch window, -// each starting from a distinct secret so identical stripes cannot cancel -// on the XOR merge. +// manually unrolled and interleaved to eliminate execution stalls. Each +// starts from a distinct secret so identical stripes cannot cancel on the XOR +// merge. // - Finalizer: keeps BOTH halves of a widening product and mixes them against // each other, with the length folded into the product operands so it // modulates the result multiplicatively (and only at finalize -- which is // what makes streaming possible). // - 128-bit: two lanes with distinct secret banks and swapped operand roles -// run over the same input (a shared 4-chain bulk tier feeds both lanes -// through different nonlinear merges); each lane covers every input byte. +// run over the same input; an interleaved 4-chain bulk tier feeds both lanes +// through different nonlinear merges where each lane covers every input byte. // - Streaming (64-bit): eagerly consumes full 128-byte blocks, keeps a // rolling window of the last 16 bytes for the overlapping tail reads; // chunked updates produce exactly the one-shot value. @@ -66,9 +66,18 @@ // The secret constants are nothing-up-my-sleeve numbers: the 64-bit // fractional parts of the square roots of the first sixteen primes (the // SHA-512 and SHA-384 initial hash values, FIPS 180-4 sections 5.3.5/5.3.4). + +#if defined(__GNUC__) || defined(__clang__) +# define MBO_FORCE_INLINE inline __attribute__((always_inline)) +#elif defined(_MSC_VER) +# define MBO_FORCE_INLINE __forceinline +#else +# define MBO_FORCE_INLINE inline +#endif + namespace mbo::hash::mumbo { -// NOLINTBEGIN(*-magic-numbers,*-pointer-arithmetic,*-easily-swappable-parameters) +// NOLINTBEGIN(*-magic-numbers,*-pointer-arithmetic,*-easily-swappable-parameters,readability-identifier-length) inline constexpr uint64_t kDefaultSeed = ::mbo::hash::kDefaultSeed; @@ -76,8 +85,10 @@ namespace mumbo_internal { using hash_internal::Load32; using hash_internal::Load64; +using hash_internal::LoadSmall; using hash_internal::Mul128Fold64; using hash_internal::Mult128; +using hash_internal::SmallInput; inline constexpr std::array kSecret = { 0x6A09E667F3BCC908, 0xBB67AE8584CAA73B, 0x3C6EF372FE94F82B, 0xA54FF53A5F1D36F1, @@ -89,46 +100,6 @@ inline constexpr std::array kSecret = { // The bulk tier's fetch window (eight 16-byte chains). inline constexpr std::size_t kBulkWindow = 128; -// Loads the (0..16 byte) small-key input into two words; see the structure -// notes in the header comment. -struct SmallInput { - uint64_t a = 0; - uint64_t b = 0; -}; - -constexpr SmallInput LoadSmall(const char* ptr, std::size_t len) noexcept { - // NOLINTNEXTLINE(readability-identifier-length): byte-widening helper. - const auto u8 = [](char chr) constexpr { return static_cast(static_cast(chr)); }; - // If-ladder, common 4..16 range gated first: the dense switch compiled to a - // jump table (indirect branch + table load) that dominated the small-key - // cost; here 9..16 (the bulk of hashed string keys, and of the SSO range) - // resolve on the first compare. Values are byte-identical to the previous - // switch at every length. - if (len >= 4) { - if (len >= 9) { // 9..16: two 64-bit loads overlapping the end. - return {.a = Load64(ptr), .b = Load64(ptr + len - 8)}; - } - if (len == 8) { - const uint64_t val = Load64(ptr); - return {.a = val, .b = val}; - } - return {.a = Load32(ptr), .b = Load32(ptr + len - 4)}; // 4..7 (len 4 -> both equal) - } - if (len == 3) { - const uint64_t val = (u8(ptr[0]) << 45U) | (u8(ptr[1]) << 8U) | u8(ptr[2]); - return {.a = val, .b = val}; - } - if (len == 2) { - const uint64_t val = (u8(ptr[0]) << 45U) | (u8(ptr[1]) << 8U) | u8(ptr[0]); - return {.a = val, .b = val}; - } - if (len == 1) { - const uint64_t val = u8(ptr[0]); - return {.a = (val << 45U) | val, .b = (val << 45U) | val}; - } - return {.a = 0, .b = 0}; -} - // The shared two-multiply finalizer. Keeps BOTH halves of the first widening // product and mixes them against each other so every final-multiply operand // carries input/seed entropy; the length sits in a product operand so it @@ -146,11 +117,36 @@ constexpr uint64_t Finish2(uint64_t val_a, uint64_t val_b, uint64_t seed, uint64 return Mul128Fold64(product.h1 ^ kSecret[11] ^ len, product.h2 ^ kSecret[9]); } -// One 128-byte bulk block over the eight chains. -constexpr void BulkBlock(std::array& chain, const char* ptr) noexcept { - for (std::size_t i = 0; i < 8; ++i) { // NOLINTNEXTLINE(*-constant-array-index) - chain[i] = Mul128Fold64(Load64(ptr + (16 * i)) ^ kSecret[4 + i], Load64(ptr + (16 * i) + 8) ^ chain[i]); - } +// One 128-byte bulk block over the eight chains. Manually unrolled and +// interleaved to maximize instruction-level parallelism (ILP) and prevent +// execution pipeline stalls while retaining identical state output. +MBO_FORCE_INLINE constexpr void BulkBlock(std::array& chain, const char* ptr) noexcept { + const uint64_t a0 = Load64(ptr + 0) ^ kSecret[4]; + const uint64_t b0 = Load64(ptr + 8) ^ chain[0]; + const uint64_t a1 = Load64(ptr + 16) ^ kSecret[5]; + const uint64_t b1 = Load64(ptr + 24) ^ chain[1]; + const uint64_t a2 = Load64(ptr + 32) ^ kSecret[6]; + const uint64_t b2 = Load64(ptr + 40) ^ chain[2]; + const uint64_t a3 = Load64(ptr + 48) ^ kSecret[7]; + const uint64_t b3 = Load64(ptr + 56) ^ chain[3]; + + const uint64_t a4 = Load64(ptr + 64) ^ kSecret[8]; + const uint64_t b4 = Load64(ptr + 72) ^ chain[4]; + const uint64_t a5 = Load64(ptr + 80) ^ kSecret[9]; + const uint64_t b5 = Load64(ptr + 88) ^ chain[5]; + const uint64_t a6 = Load64(ptr + 96) ^ kSecret[10]; + const uint64_t b6 = Load64(ptr + 104) ^ chain[6]; + const uint64_t a7 = Load64(ptr + 112) ^ kSecret[11]; + const uint64_t b7 = Load64(ptr + 120) ^ chain[7]; + + chain[0] = Mul128Fold64(a0, b0); + chain[1] = Mul128Fold64(a1, b1); + chain[2] = Mul128Fold64(a2, b2); + chain[3] = Mul128Fold64(a3, b3); + chain[4] = Mul128Fold64(a4, b4); + chain[5] = Mul128Fold64(a5, b5); + chain[6] = Mul128Fold64(a6, b6); + chain[7] = Mul128Fold64(a7, b7); } constexpr std::array BulkInit(uint64_t seed) noexcept { @@ -206,9 +202,6 @@ constexpr uint64_t GetHash64(std::string_view str, uint64_t seed = kDefaultSeed) return mumbo_internal::Finish(val_a, val_b, seed, len); } -// Native 128-bit form (fronted by the `jumbo` namespace below): two lanes -// with different secrets and swapped operand roles cover the same input; see -// the header comment. // NOLINTNEXTLINE(readability-function-cognitive-complexity): tiered by design. constexpr Hash128 GetHash128(std::string_view str, uint64_t seed = kDefaultSeed) noexcept { const char* ptr = str.data(); @@ -230,15 +223,26 @@ constexpr Hash128 GetHash128(std::string_view str, uint64_t seed = kDefaultSeed) if (len >= 64) { // Shared 4-chain bulk tier: each lane derives from ALL chains, through // different (nonlinear) merges, so both halves cover every input byte. + // Interleaved manually here to eliminate execution pipeline bottlenecks. uint64_t chain0 = seed1 ^ kSecret[8]; uint64_t chain1 = seed1 ^ kSecret[9]; uint64_t chain2 = seed2 ^ kSecret[10]; uint64_t chain3 = seed2 ^ kSecret[11]; while (remaining >= 64) { - chain0 = Mul128Fold64(Load64(ptr) ^ kSecret[4], Load64(ptr + 8) ^ chain0); - chain1 = Mul128Fold64(Load64(ptr + 16) ^ kSecret[5], Load64(ptr + 24) ^ chain1); - chain2 = Mul128Fold64(Load64(ptr + 32) ^ kSecret[6], Load64(ptr + 40) ^ chain2); - chain3 = Mul128Fold64(Load64(ptr + 48) ^ kSecret[7], Load64(ptr + 56) ^ chain3); + const uint64_t a0 = Load64(ptr) ^ kSecret[4]; + const uint64_t b0 = Load64(ptr + 8) ^ chain0; + const uint64_t a1 = Load64(ptr + 16) ^ kSecret[5]; + const uint64_t b1 = Load64(ptr + 24) ^ chain1; + const uint64_t a2 = Load64(ptr + 32) ^ kSecret[6]; + const uint64_t b2 = Load64(ptr + 40) ^ chain2; + const uint64_t a3 = Load64(ptr + 48) ^ kSecret[7]; + const uint64_t b3 = Load64(ptr + 56) ^ chain3; + + chain0 = Mul128Fold64(a0, b0); + chain1 = Mul128Fold64(a1, b1); + chain2 = Mul128Fold64(a2, b2); + chain3 = Mul128Fold64(a3, b3); + ptr += 64; remaining -= 64; } @@ -357,7 +361,7 @@ struct Algorithm { } }; -// NOLINTEND(*-magic-numbers,*-pointer-arithmetic,*-easily-swappable-parameters) +// NOLINTEND(*-magic-numbers,*-pointer-arithmetic,*-easily-swappable-parameters,readability-identifier-length) } // namespace mbo::hash::mumbo @@ -383,4 +387,6 @@ struct Algorithm : ::mbo::hash::mumbo::Algorithm {}; } // namespace mbo::hash::jumbo +#undef MBO_FORCE_INLINE + #endif // MBO_HASH_HASH_MUMBO_H_ From f4d514a33cfc3c21d8cc0620855ebbdd4b4531b4 Mon Sep 17 00:00:00 2001 From: helly25 <6420169+helly25@users.noreply.github.com> Date: Sun, 19 Jul 2026 10:42:47 +0100 Subject: [PATCH 2/2] WS --- mbo/hash/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mbo/hash/README.md b/mbo/hash/README.md index c018760..9961a3b 100644 --- a/mbo/hash/README.md +++ b/mbo/hash/README.md @@ -814,7 +814,7 @@ benchmark plus both SMHasher3 batteries): parallelism (ILP) and resolves hardware pipeline execution stalls without altering the underlying hash values. Unfortunately even in 2026 the algorithm uses more registers than compilers can use for full ILP, so either a redesign - or altogether new algorithm is needed. + or altogether new algorithm is needed. ### dumbo: the measured design iterations