From 1054c8e72c26440963d24fec492f2675259747e8 Mon Sep 17 00:00:00 2001 From: Arthur Silva Date: Thu, 25 Jun 2026 00:09:44 +0200 Subject: [PATCH 1/3] Hide default hasher behind opaque DefaultHashBuilder newtype BREAKING (0.7.0). `DefaultHashBuilder` was a public type alias for `ahash::RandomState`, leaking the concrete hasher; the `ahash` feature leaked it too. Both are now implementation-agnostic: - `DefaultHashBuilder` is an opaque newtype wrapping a private, cfg-selected inner hasher. The algorithm can change with no public-API churn. - The `ahash` feature is renamed to `custom-hasher` and pulls the hasher crate via `dep:ahash`, so the crate name no longer appears as a feature. Migration: replace feature `ahash` with `custom-hasher`; don't name the concrete hasher behind `DefaultHashBuilder`. --- Cargo.toml | 5 ++++- src/lib.rs | 43 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f3700d6..1fd2b46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,10 @@ exclude = ["fuzz"] rust-version = "1.71" [features] -default = ["ahash", "parking_lot"] +default = ["custom-hasher", "parking_lot"] +# Use the crate's bundled fast default hasher (currently ahash) behind the +# opaque `DefaultHashBuilder`. The concrete hasher is an implementation detail. +custom-hasher = ["dep:ahash"] sharded-lock = ["dep:crossbeam-utils"] shuttle = ["dep:shuttle"] stats = [] diff --git a/src/lib.rs b/src/lib.rs index 4db978a..00b793f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,9 +59,11 @@ //! //! # Hasher //! -//! By default the crate uses [ahash](https://crates.io/crates/ahash), which is enabled (by default) via -//! a crate feature with the same name. If the `ahash` feature is disabled the crate defaults to the std lib -//! implementation instead (currently Siphash13). Note that a custom hasher can also be provided if desirable. +//! By default the crate uses a fast non-cryptographic hasher (currently [ahash](https://crates.io/crates/ahash)) +//! exposed through the opaque [`DefaultHashBuilder`] type and enabled via the `custom-hasher` feature (on by +//! default). The concrete algorithm is an implementation detail and may change between releases. If the +//! `custom-hasher` feature is disabled the crate falls back to the std lib implementation instead (currently +//! Siphash13). Note that a custom hasher can also be provided via the cache's `B` type parameter if desirable. //! //! # Synchronization primitives //! @@ -75,7 +77,7 @@ //! //! | Feature | Default | Description | //! |---------|---------|-------------| -//! | `ahash` | ✓ | Use [ahash](https://crates.io/crates/ahash) as the default hasher. When disabled, falls back to std lib's `RandomState` (currently SipHash-1-3). | +//! | `custom-hasher` | ✓ | Use the crate's bundled fast non-cryptographic default hasher (currently [ahash](https://crates.io/crates/ahash)) behind the opaque [`DefaultHashBuilder`]. When disabled, falls back to std lib's `RandomState` (currently SipHash-1-3). | //! | `parking_lot` | ✓ | Use [parking_lot](https://crates.io/crates/parking_lot) for synchronization primitives. Mutually exclusive with `sharded-lock`. | //! | `sharded-lock` | | Use [`crossbeam_utils::sync::ShardedLock`](https://docs.rs/crossbeam-utils/latest/crossbeam_utils/sync/struct.ShardedLock.html) for synchronization primitives. Mutually exclusive with `parking_lot`. | //! | `shuttle` | | Enable [shuttle](https://crates.io/crates/shuttle) testing support for concurrency testing. | @@ -107,10 +109,35 @@ mod shuttle_tests; pub use options::{Options, OptionsBuilder}; -#[cfg(feature = "ahash")] -pub type DefaultHashBuilder = ahash::RandomState; -#[cfg(not(feature = "ahash"))] -pub type DefaultHashBuilder = std::collections::hash_map::RandomState; +#[cfg(feature = "custom-hasher")] +type DefaultHasherImpl = ahash::RandomState; +#[cfg(not(feature = "custom-hasher"))] +type DefaultHasherImpl = std::collections::hash_map::RandomState; + +/// The default [`BuildHasher`](std::hash::BuildHasher) used by the cache. +/// +/// The hashing algorithm behind this type is an implementation detail and may +/// change in any release. Name this type — don't rely on the concrete hasher +/// it wraps. The underlying hasher is selected by the `custom-hasher` feature +/// (a fast non-cryptographic hasher when enabled, the std library's +/// `RandomState` otherwise). +#[derive(Clone, Default)] +pub struct DefaultHashBuilder(DefaultHasherImpl); + +impl std::hash::BuildHasher for DefaultHashBuilder { + type Hasher = ::Hasher; + + #[inline] + fn build_hasher(&self) -> Self::Hasher { + self.0.build_hasher() + } +} + +impl std::fmt::Debug for DefaultHashBuilder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DefaultHashBuilder").finish_non_exhaustive() + } +} /// Defines the weight of a cache entry. /// From 8217dc2db97b1454bfb20e689de780cfd52981a7 Mon Sep 17 00:00:00 2001 From: Arthur Silva Date: Thu, 25 Jun 2026 00:13:32 +0200 Subject: [PATCH 2/3] Switch default hasher from ahash to foldhash (quality) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercises the DefaultHashBuilder abstraction: swapping the inner hasher touches only the private `DefaultHasherImpl` alias and the optional dependency — the public `DefaultHashBuilder` type and the `custom-hasher` feature are unchanged. foldhash's `quality` variant gives stronger distribution than its `fast` variant while staying far faster than the std SipHash fallback. --- Cargo.toml | 6 +++--- src/lib.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1fd2b46..c2a6f54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,17 +14,17 @@ rust-version = "1.71" [features] default = ["custom-hasher", "parking_lot"] -# Use the crate's bundled fast default hasher (currently ahash) behind the +# Use the crate's bundled fast default hasher (currently foldhash) behind the # opaque `DefaultHashBuilder`. The concrete hasher is an implementation detail. -custom-hasher = ["dep:ahash"] +custom-hasher = ["dep:foldhash"] sharded-lock = ["dep:crossbeam-utils"] shuttle = ["dep:shuttle"] stats = [] [dependencies] -ahash = { optional = true, version = "0.8" } crossbeam-utils = { optional = true, version = "0.8" } equivalent = "1.0" +foldhash = { optional = true, version = "0.2" } hashbrown = { version = "0.16", default-features = false, features = ["inline-more"] } parking_lot = { optional = true, version = "0.12" } shuttle = { version = "0.8", optional = true } diff --git a/src/lib.rs b/src/lib.rs index 00b793f..ff096de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,7 +59,7 @@ //! //! # Hasher //! -//! By default the crate uses a fast non-cryptographic hasher (currently [ahash](https://crates.io/crates/ahash)) +//! By default the crate uses a fast non-cryptographic hasher (currently [foldhash](https://crates.io/crates/foldhash)) //! exposed through the opaque [`DefaultHashBuilder`] type and enabled via the `custom-hasher` feature (on by //! default). The concrete algorithm is an implementation detail and may change between releases. If the //! `custom-hasher` feature is disabled the crate falls back to the std lib implementation instead (currently @@ -77,7 +77,7 @@ //! //! | Feature | Default | Description | //! |---------|---------|-------------| -//! | `custom-hasher` | ✓ | Use the crate's bundled fast non-cryptographic default hasher (currently [ahash](https://crates.io/crates/ahash)) behind the opaque [`DefaultHashBuilder`]. When disabled, falls back to std lib's `RandomState` (currently SipHash-1-3). | +//! | `custom-hasher` | ✓ | Use the crate's bundled fast non-cryptographic default hasher (currently [foldhash](https://crates.io/crates/foldhash)) behind the opaque [`DefaultHashBuilder`]. When disabled, falls back to std lib's `RandomState` (currently SipHash-1-3). | //! | `parking_lot` | ✓ | Use [parking_lot](https://crates.io/crates/parking_lot) for synchronization primitives. Mutually exclusive with `sharded-lock`. | //! | `sharded-lock` | | Use [`crossbeam_utils::sync::ShardedLock`](https://docs.rs/crossbeam-utils/latest/crossbeam_utils/sync/struct.ShardedLock.html) for synchronization primitives. Mutually exclusive with `parking_lot`. | //! | `shuttle` | | Enable [shuttle](https://crates.io/crates/shuttle) testing support for concurrency testing. | @@ -110,7 +110,7 @@ mod shuttle_tests; pub use options::{Options, OptionsBuilder}; #[cfg(feature = "custom-hasher")] -type DefaultHasherImpl = ahash::RandomState; +type DefaultHasherImpl = foldhash::quality::RandomState; #[cfg(not(feature = "custom-hasher"))] type DefaultHasherImpl = std::collections::hash_map::RandomState; From 68c44600ee2a38c042eda1e3700955e312fc4ace Mon Sep 17 00:00:00 2001 From: Arthur Silva Date: Thu, 25 Jun 2026 01:20:38 +0200 Subject: [PATCH 3/3] Document why compute_shard_index rotates by usize::BITS, not u64::BITS --- src/sync.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/sync.rs b/src/sync.rs index 54f5cfe..e71fec2 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -229,6 +229,12 @@ impl< // shard, rotate the hash by usize::BITS / 2 so we avoid the lower bits and // the highest 7 bits that hashbrown uses internally for probing, improving // the real entropy available to each hashbrown shard. + // + // The rotation is deliberately tied to usize::BITS, not u64::BITS: on 32-bit + // targets a usize-width hasher (e.g. FxHash) leaves the top 32 hash bits zero + // (hashbrown guards against this too; see its h1 / Tag::full), so we keep the + // shard window in the low word where the entropy actually lives. Rotating by + // u64::BITS / 2 there would send every key to shard 0. hash.rotate_right(usize::BITS / 2) & self.shards_mask }