Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Hash for f32 and f64 only. #168

Merged
merged 1 commit into from
Feb 17, 2025
Merged
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
104 changes: 59 additions & 45 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,6 @@ pub use num_traits::{Float, Pow};
#[cfg(feature = "rand")]
pub use impl_rand::{UniformNotNan, UniformOrdered};

// masks for the parts of the IEEE 754 float
const SIGN_MASK: u64 = 0x8000000000000000u64;
const EXP_MASK: u64 = 0x7ff0000000000000u64;
const MAN_MASK: u64 = 0x000fffffffffffffu64;

// canonical raw bit patterns (for hashing)
const CANONICAL_NAN_BITS: u64 = 0x7ff8000000000000u64;

#[inline(always)]
fn canonicalize_signed_zero<T: FloatCore>(x: T) -> T {
// -0.0 + 0.0 == +0.0 under IEEE754 roundTiesToEven rounding mode,
// which Rust guarantees. Thus by adding a positive zero we
// canonicalize signed zero without any branches in one instruction.
x + T::zero()
}

/// A wrapper around floats providing implementations of `Eq`, `Ord`, and `Hash`.
///
/// NaN is sorted as *greater* than all other values and *equal*
Expand Down Expand Up @@ -332,18 +316,6 @@ impl<T: FloatCore> PartialEq<T> for OrderedFloat<T> {
}
}

impl<T: FloatCore> Hash for OrderedFloat<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
let bits = if self.is_nan() {
CANONICAL_NAN_BITS
} else {
raw_double_bits(&canonicalize_signed_zero(self.0))
};

bits.hash(state)
}
}

impl<T: fmt::Debug> fmt::Debug for OrderedFloat<T> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Expand Down Expand Up @@ -1350,14 +1322,6 @@ impl<T: FloatCore> Ord for NotNan<T> {
}
}

impl<T: FloatCore> Hash for NotNan<T> {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
let bits = raw_double_bits(&canonicalize_signed_zero(self.0));
bits.hash(state)
}
}

impl<T: fmt::Debug> fmt::Debug for NotNan<T> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Expand Down Expand Up @@ -1790,15 +1754,6 @@ impl From<FloatIsNan> for std::io::Error {
}
}

#[inline]
/// Used for hashing. Input must not be zero or NaN.
fn raw_double_bits<F: FloatCore>(f: &F) -> u64 {
let (man, exp, sign) = f.integer_decode();
let exp_u64 = exp as u16 as u64;
let sign_u64 = (sign > 0) as u64;
(man & MAN_MASK) | ((exp_u64 << 52) & EXP_MASK) | ((sign_u64 << 63) & SIGN_MASK)
}

impl<T: FloatCore> Zero for NotNan<T> {
#[inline]
fn zero() -> Self {
Expand Down Expand Up @@ -2039,6 +1994,65 @@ impl_float_const!(OrderedFloat, OrderedFloat);
// Float constants are not NaN.
impl_float_const!(NotNan, |x| unsafe { NotNan::new_unchecked(x) });

// canonical raw bit patterns (for hashing)

mod hash_internals {
pub trait SealedTrait: Copy + num_traits::float::FloatCore {
type Bits: core::hash::Hash;

const CANONICAL_NAN_BITS: Self::Bits;

fn canonical_bits(self) -> Self::Bits;
}

impl SealedTrait for f32 {
type Bits = u32;

const CANONICAL_NAN_BITS: u32 = 0x7fc00000;

fn canonical_bits(self) -> u32 {
// -0.0 + 0.0 == +0.0 under IEEE754 roundTiesToEven rounding mode,
// which Rust guarantees. Thus by adding a positive zero we
// canonicalize signed zero without any branches in one instruction.
(self + 0.0).to_bits()
}
}

impl SealedTrait for f64 {
type Bits = u64;

const CANONICAL_NAN_BITS: u64 = 0x7ff8000000000000;

fn canonical_bits(self) -> u64 {
(self + 0.0).to_bits()
}
}
}

/// The built-in floating point types `f32` and `f64`.
///
/// This is a "sealed" trait that cannot be implemented for any other types.
pub trait PrimitiveFloat: hash_internals::SealedTrait {}
impl PrimitiveFloat for f32 {}
impl PrimitiveFloat for f64 {}

impl<T: PrimitiveFloat> Hash for OrderedFloat<T> {
fn hash<H: Hasher>(&self, hasher: &mut H) {
let bits = if self.0.is_nan() {
T::CANONICAL_NAN_BITS
} else {
self.0.canonical_bits()
};
bits.hash(hasher);
}
}
Comment on lines +2039 to +2048
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks the original hash result because it doesn't call raw_double_bits anymore.

Our use case depends on stable hash results. @mbrubeck would you give some background why change the result? (I can understand we implement only for f32/f64)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about that. The main reason for that change was performance. (See #142 for details.)


impl<T: PrimitiveFloat> Hash for NotNan<T> {
fn hash<H: Hasher>(&self, hasher: &mut H) {
self.0.canonical_bits().hash(hasher);
}
}

#[cfg(feature = "serde")]
mod impl_serde {
extern crate serde;
Expand Down