Skip to content

Commit

Permalink
Merge pull request #144 from orlp/faster-ord
Browse files Browse the repository at this point in the history
Optimize ord implementation and signed zero canonicalization
  • Loading branch information
mbrubeck committed Oct 10, 2023
2 parents f4ca84b + 71770b3 commit 4e29b08
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 25 deletions.
68 changes: 43 additions & 25 deletions src/lib.rs
Expand Up @@ -41,7 +41,14 @@ const MAN_MASK: u64 = 0x000fffffffffffffu64;

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

#[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`.
///
Expand Down Expand Up @@ -116,25 +123,43 @@ impl<T: FloatCore> PartialOrd for OrderedFloat<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}

#[inline]
fn lt(&self, other: &Self) -> bool {
!self.ge(other)
}

#[inline]
fn le(&self, other: &Self) -> bool {
other.ge(self)
}

#[inline]
fn gt(&self, other: &Self) -> bool {
!other.ge(self)
}

#[inline]
fn ge(&self, other: &Self) -> bool {
// We consider all NaNs equal, and NaN is the largest possible
// value. Thus if self is NaN we always return true. Otherwise
// self >= other is correct. If other is also not NaN it is trivially
// correct, and if it is we note that nothing can be greater or
// equal to NaN except NaN itself, which we already handled earlier.
self.0.is_nan() | (self.0 >= other.0)
}
}

impl<T: FloatCore> Ord for OrderedFloat<T> {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
let lhs = &self.0;
let rhs = &other.0;
match lhs.partial_cmp(rhs) {
Some(ordering) => ordering,
None => {
if lhs.is_nan() {
if rhs.is_nan() {
Ordering::Equal
} else {
Ordering::Greater
}
} else {
Ordering::Less
}
}
#[allow(clippy::comparison_chain)]
if self < other {
Ordering::Less
} else if self > other {
Ordering::Greater
} else {
Ordering::Equal
}
}
}
Expand All @@ -161,10 +186,8 @@ 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 if self.is_zero() {
CANONICAL_ZERO_BITS
} else {
raw_double_bits(&self.0)
raw_double_bits(&canonicalize_signed_zero(self.0))
};

bits.hash(state)
Expand Down Expand Up @@ -1150,12 +1173,7 @@ 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 = if self.is_zero() {
CANONICAL_ZERO_BITS
} else {
raw_double_bits(&self.0)
};

let bits = raw_double_bits(&canonicalize_signed_zero(self.0));
bits.hash(state)
}
}
Expand Down
26 changes: 26 additions & 0 deletions tests/test.rs
Expand Up @@ -21,6 +21,32 @@ fn not_nan<T: FloatCore>(x: T) -> NotNan<T> {
NotNan::new(x).unwrap()
}

#[test]
fn test_total_order() {
let numberline = [
(-f32::INFINITY, 0),
(-1.0, 1),
(-0.0, 2),
(0.0, 2),
(1.0, 3),
(f32::INFINITY, 4),
(f32::NAN, 5),
(-f32::NAN, 5),
];

for &(fi, i) in &numberline {
for &(fj, j) in &numberline {
assert_eq!(OrderedFloat(fi) < OrderedFloat(fj), i < j);
assert_eq!(OrderedFloat(fi) > OrderedFloat(fj), i > j);
assert_eq!(OrderedFloat(fi) <= OrderedFloat(fj), i <= j);
assert_eq!(OrderedFloat(fi) >= OrderedFloat(fj), i >= j);
assert_eq!(OrderedFloat(fi) == OrderedFloat(fj), i == j);
assert_eq!(OrderedFloat(fi) != OrderedFloat(fj), i != j);
assert_eq!(OrderedFloat(fi).cmp(&OrderedFloat(fj)), i.cmp(&j));
}
}
}

#[test]
fn ordered_f32_compare_regular_floats() {
assert_eq!(OrderedFloat(7.0f32).cmp(&OrderedFloat(7.0)), Equal);
Expand Down

0 comments on commit 4e29b08

Please sign in to comment.