Skip to content

Fix Ord and PartialOrd differing for FloatOrd and optimize implementation#12711

Merged
alice-i-cecile merged 3 commits intobevyengine:mainfrom
Kanabenki:optim-float-ord
Mar 27, 2024
Merged

Fix Ord and PartialOrd differing for FloatOrd and optimize implementation#12711
alice-i-cecile merged 3 commits intobevyengine:mainfrom
Kanabenki:optim-float-ord

Conversation

@Kanabenki
Copy link
Contributor

@Kanabenki Kanabenki commented Mar 25, 2024

Objective

  • FloatOrd currently has a different comparison behavior between its derived PartialOrd impl and manually implemented Ord impl (The Ord doc says this is a logic error). This might be a problem for some std containers/algorithms if they rely on both matching, and a footgun for Bevy users.

Solution

  • Replace the PartialEq and Ord impls of FloatOrd with some equivalent ones producing better assembly.
  • Manually derive PartialOrd with the same behavior as Ord, implement the comparison operators.
  • Add some tests.

I first tried using a match-based implementation similar to the PartialOrd impl of the std (with added NaN ordering) but I couldn't get it to produce non-branching assembly. The current implementation is based on the one from the ordered_float crate, adapted since it uses a different ordering. Should this be mentionned somewhere in the code?


Changelog

Fixed

  • FloatOrd now uses the same ordering for its PartialOrd and Ord implementations.

Migration Guide

  • If you were depending on the PartialOrd behaviour of FloatOrd, it has changed from matching f32 to matching FloatOrd's Ord ordering, never returning None.

@alice-i-cecile alice-i-cecile added C-Bug An unexpected or incorrect behavior A-Math Fundamental domain-agnostic mathematical operations A-Utils Utility functions and types labels Mar 25, 2024
@alice-i-cecile alice-i-cecile added the M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide label Mar 25, 2024
assert_eq!(one.cmp(&zero), Ordering::Greater);
assert_eq!(zero.cmp(&one), Ordering::Less);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

could we add some tests for le too, since the behavior there is a little weird and should match cmp?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done for all operators, also added a test for the Hash impl.

Copy link
Contributor

@bushrat011899 bushrat011899 left a comment

Choose a reason for hiding this comment

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

I think this is a good idea, but I personally don't like that the core implementation of the behaviour is split between PartialOrd::le and PartialEq::eq. If instead we provide an explicit implementation for Ord::cmp, all the other methods can be derived/implied:

// Actual Implementation
impl Ord for FloatOrd {
    fn cmp(&self, other: &Self) -> Ordering {
        self.0.partial_cmp(&other.0).unwrap_or_else(|| {
            // Comparison could only fail if one of self or other is NaN.
            // All NaN values are equivalent and less than all non-NaN values
            if !other.0.is_nan() {
                Ordering::Less
            } else if !self.0.is_nan() {
                Ordering::Greater
            } else {
                Ordering::Equal
            }
        })
    }
}

// Derived from Above
impl PartialEq for FloatOrd {
    fn eq(&self, other: &Self) -> bool {
        self.cmp(other).is_eq()
    }
}

impl PartialOrd for FloatOrd {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Eq for FloatOrd {}

I personally prefer this, since there is only 1 function to actually test and reason around, and it's very clear how it relates to all other comparisons.

@Kanabenki
Copy link
Contributor Author

I think this is a good idea, but I personally don't like that the core implementation of the behaviour is split between PartialOrd::le and PartialEq::eq. If instead we provide an explicit implementation for Ord::cmp, all the other methods can be derived/implied:

I agree that this would be easier to maintain and that was the very first thing I tried, but the produced assembly is worse (notably, having cmp implemented in term of the manually implemented comparison operators instead of the other way around allows the operators to be better optimized). Since FloatOrd is used as a sorting key in rendering, I think would make sense to try to keep it as optimized as possible, even if the resulting impl is a bit less straightforward.

It seems that keeping the current cmp impl of this PR but deferring to it for Eq also produce worse assembly for eq.

@alice-i-cecile
Copy link
Member

This will conflict with #12732: we should be careful not to lose this work in the process.

@bushrat011899
Copy link
Contributor

I agree that this would be easier to maintain and that was the very first thing I tried, but the produced assembly is worse

Unfortunate, but I completely agree. The performance of this item is important enough to make the sacrifice reasonable.

@alice-i-cecile alice-i-cecile added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Mar 27, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Mar 27, 2024
Merged via the queue into bevyengine:main with commit 025e8e6 Mar 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Math Fundamental domain-agnostic mathematical operations A-Utils Utility functions and types C-Bug An unexpected or incorrect behavior M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants