Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upAdd a total ordering method for floating-point #53938
Conversation
rust-highfive
assigned
Mark-Simulacrum
Sep 4, 2018
This comment has been minimized.
This comment has been minimized.
|
(rust_highfive has picked a reviewer for you, use r? to override) |
rust-highfive
added
the
S-waiting-on-review
label
Sep 4, 2018
kennytm
added
the
T-libs
label
Sep 4, 2018
rkruppe
requested changes
Sep 4, 2018
Thank you so much for tackling this! Unfortunately I have lots of nits wrt the docs, in particular the bullet points defining the order. I have a different approach in mind for that part, I'll write it up and post it here for comparison rather than ask you to reword each bullet point individually.
| /// | ||
| /// Because of negative zero and NaNs, the ordinary comparison operators | ||
| /// for `f32` do not represent a total order. This method, however, | ||
| /// defines an ordering between all distinct bit patterns: |
This comment has been minimized.
This comment has been minimized.
rkruppe
Sep 4, 2018
Contributor
I don't think we'd want to mention this, since Rust pretty much ignores non-canonical floats anyway, but I'll mention it for completeness: totalOrder only distinguishes all bit patterns of the exchange format (binary32), the arithmetic format may have multiple distinct encodings of the same floating point datum, which would then all be considered equal.
This comment has been minimized.
This comment has been minimized.
jpetkau
Sep 4, 2018
What do you mean by "non-canonical floats"? Only +0 and -0 are distinct encodings of sorta-equal numbers, and they are explicitly distinct in this ordering. There are no other cases where two distinct bit patterns represent equal IEEE754 floats.
This comment has been minimized.
This comment has been minimized.
rkruppe
Sep 4, 2018
Contributor
This is true of the standard/basic formats (in radix 2 at least), but an implementation may perform operations on a different format (the "arithmetic format") and provide conversions between that format and the interchange formats, and then the arithmetic format may have multiple encodings for what would be one datum with only one encoding in the basic format. Some examples can be found in https://llvm.org/docs/LangRef.html#llvm-canonicalize-intrinsic
| /// for `f32` do not represent a total order. This method, however, | ||
| /// defines an ordering between all distinct bit patterns: | ||
| /// | ||
| /// - For normal numbers, the ordering matches the comparison operators. |
This comment has been minimized.
This comment has been minimized.
rkruppe
Sep 4, 2018
Contributor
"Normal" has a specific meaning for floats which you probably don't want here (it excludes subnormals and infinities, which totalOrder also handles exactly like the ordinary comparisons). It also isn't great for lay people since it's not obvious that "normal" excludes zeros.
| /// defines an ordering between all distinct bit patterns: | ||
| /// | ||
| /// - For normal numbers, the ordering matches the comparison operators. | ||
| /// - Zeros are between the positive and negative normal numbers, with |
This comment has been minimized.
This comment has been minimized.
rkruppe
Sep 4, 2018
•
Contributor
It took me a moment to parse this sentence as {negative non-zero} < -0.0 < +0.0 < {positive non-zero} (see above comment re: "normal"). It's also not great that this makes the ordering between zeros and non-zero finite numbers sound like a deviation from the conventional comparisons when really only the distinction between -0 and +0 is new.
| /// - Zeros are between the positive and negative normal numbers, with | ||
| /// the positive zero greater than the negative zero. | ||
| /// - Positive infinity is greater than all normal numbers; negative | ||
| /// infinity is less than all normal numbers. |
This comment has been minimized.
This comment has been minimized.
rkruppe
Sep 4, 2018
Contributor
This is in line with the usual comparison operators, and again "normal" is the wrong (overly narrow) term here.
| /// - Positive infinity is greater than all normal numbers; negative | ||
| /// infinity is less than all normal numbers. | ||
| /// - Positive NaNs are greater than all other values, ordered amongst | ||
| /// themselves by their payloads; Negative NaNs are less than all other |
This comment has been minimized.
This comment has been minimized.
rkruppe
Sep 4, 2018
Contributor
Note that NaNs are ordered by signaling bit first, payload second (and the signaling bit is not part of the payload in official terms). I believe with the 2008 definition of the signaling bit (1 for quiet, 0 for signaling) this turns out the same as ordering by the significand bits, but since we did in the past take at least a little care about every existing Mips chip having this bit the other way around, maybe we should be a bit more precise (or maybe even use a different implementation on Mips to respect its interpretation of the signaling bit).
cc @est31
This comment has been minimized.
This comment has been minimized.
est31
Sep 4, 2018
Contributor
maybe even use a different implementation on Mips to respect its interpretation of the signaling bit
It seems that the totalOrder operation got introduced by the 2008 edition of IEEE 754, i.e. it wasn't present in the 1985 edition. The totalOrder operation is specified in terms of the payload and the signaling bit, and not the signifigand bits. If we implement the operation on older MIPS, which is only compliant with the 1985 edition, we are basically "backporting" the definition in the 2008 standard to the 1985 standard. If we don't adjust the parts about the signaling bit, I think this goes really wrong: I think that the spec authors have carefully made the totalOrder rules so that they can be implemented by a simple operation on the binary representation. The totalOrder spec was made with the 2008 definition in mind, not with the interpretation of 1985 that MIPS chose. We should follow that spirit on MIPS as well and take the implementation that is a simple operation not a "if this is NAN then flip this bit and then do $op" one.
I don't think that @rkruppe is considering this too seriously, but just wanted to say this.
And yeah, we should document this choice about the MIPS+NaN behaviour and maybe say that it might be changed it in the future, to keep options open.
| /// a.sort_by(|&x, &y| f32::total_cmp(x, y)); | ||
| /// assert_eq!( | ||
| /// format!("{:?}", a), | ||
| /// "[NaN, -inf, -1.0, -0.0, 0.0, 1.0, inf, NaN]" |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
| fn total_order_key(self) -> i32 { | ||
| let x = self.to_bits() as i32; | ||
| // Flip the bottom 31 bits if the high bit is set | ||
| x ^ ((x >> 31) as u32 >> 1) as i32 |
This comment has been minimized.
This comment has been minimized.
rkruppe
Sep 4, 2018
Contributor
This really, really needs either an in-depth explanation justifying its correctness, or a reference to an external write-up of such an explanation. It sounds really plausible but I'm also kind of unsure about various aspects, and I've already spent more time thinking about this order and about the binary exchange format than any human being should.
This comment has been minimized.
This comment has been minimized.
scottmcm
Sep 4, 2018
Member
I don't have a proof for this, but it's equivalent to the one in rust-lang/rfcs#1249 (comment). I suspect the predicate was chosen specifically to allow such a simple implementation, because of things like "the same floating-point datum" being ordered by exponent first.
This comment has been minimized.
This comment has been minimized.
rkruppe
Sep 4, 2018
Contributor
I have the exact same question for @jpetkau then
This comment has been minimized.
This comment has been minimized.
jpetkau
Sep 4, 2018
How about something like:
"With their bits interpreted as signed integers, positive IEEE754 floats are ordered as:
+0 < (positive finite floats in order) < +inf < +NANs.
With the reverse order for floats with the sign bit set.
So if we reverse the order of the negative cases, by xoring the sign bit with the other bits, we get the order:
-NANs < -inf < (negative finite floats in order) < -0 < +0 < (positive finite floats in order) < +inf < +NANs.
IEEE754.2008 calls this the 'totalOrder' predicate.
(It also specifies an interpretation of the signaling bit which implies -QNANs < -SNANs < +SNANs < +QNANs, but if we happen to be on an older MIPS architecture with the opposite interpretation of the signaling bit, this won't be true.)
This comment has been minimized.
This comment has been minimized.
rkruppe
Sep 9, 2018
Contributor
That sounds mostly convincing! The main thing that isn't obvious to me is why negating the non-sign bits reverses the order but maybe that's just someone you have to convince yourself of by spot checks.
This comment has been minimized.
This comment has been minimized.
rkruppe
Oct 25, 2018
Contributor
It's true that this reinterprets the whole 32- or 64-bit unit, but it's possible that an architecture uses different endianness for floats and ints, then the bytes will appear reversed when stored to memory as a float and loaded back as an integer. However, I don't think we currently support such architectures, and if we wanted to, we'd have to update more code than just this method (in particular to_bits, which would automatically make this code work correctly too).
This comment has been minimized.
This comment has been minimized.
jpetkau
Oct 25, 2018
•
I dispute that it's possible that an architecture uses different endianness for floats and ints. That would be deeply insane and break a lot more than this code.
[edit] of course it's technically possible, just like an architecture could use different endianess for signed and unsigned ints if it really wanted to be perverse.
This comment has been minimized.
This comment has been minimized.
rkruppe
Oct 25, 2018
Contributor
What do you mean you dispute that it's possible? There's real physical processors (old ARMs) doing so. We don't support them, and I would be open to concluding we don't ever want to support them (just like e.g. architectures with non-octet memory granularity – also breaks tons of code, but definitely a real thing!), but they exist.
This comment has been minimized.
This comment has been minimized.
jpetkau
Oct 25, 2018
Sorry, I was just being flippant there. Such architectures do exist. But they would break far more than just this code; they'd break everything that relied on the bit pattern of floats, which is, well, everything. Every serialization library, formatter, parser, implementation of math libraries, you name it.
This comment has been minimized.
This comment has been minimized.
gnzlbg
Oct 26, 2018
Contributor
Endianness doesn't matter, because an f32 and the i32 will still have the same endianness as each other.
Thanks for the answer, I just wasn't sure if this had to be the case, but it seems that for Rust this will need to be the case.
| /// | ||
| /// # Examples | ||
| /// | ||
| /// Normal numbers: |
This comment has been minimized.
This comment has been minimized.
rkruppe
Sep 4, 2018
Contributor
If this is the section demonstrating how this function agrees with the PartialOrd impl, I'm missing:
- Comparisons between non-zero finite numbers and zeros
- Comparisons between infinities and finite numbers
| /// assert_eq!(f32::total_cmp(-2.0, -1.0), Less); | ||
| /// ``` | ||
| /// | ||
| /// Zeros, infinities, and NaNs: |
This comment has been minimized.
This comment has been minimized.
rkruppe
Sep 4, 2018
Contributor
As mentioned above infinities are not any different from the PartialOrd impl so it's misleading to group them in with zeros and NaNs imo.
| /// | ||
| /// assert_eq!(f32::partial_cmp(&std::f32::NAN, &std::f32::NAN), None); | ||
| /// assert_eq!(f32::total_cmp(-std::f32::NAN, std::f32::NAN), Less); | ||
| /// assert_eq!(f32::total_cmp(std::f32::NAN, std::f32::NAN), Equal); |
This comment has been minimized.
This comment has been minimized.
rkruppe
Sep 4, 2018
Contributor
from_bits.
This comment has been minimized.
This comment has been minimized.
|
How about we document the total order by first summarizing how it almost entirely agrees with PartialOrd and how it orders NaNs, then explicitly listing all relevant groups of floating point data in the order they're put in. Something like this: This function mostly agrees with NaNs with positive sign are ordered greater than all other floating-point values including positive infinity, while NaNs with negative sign are ordered the least, below negative infinity. Two different NaNs with the same sign are ordered first by whether the are signaling (signaling is less than quiet if the sign is positive, reverse if negative) and second by their payload interpreted as integer (reversed order if the sign is negative). This means all different (canonical) floating point bit patterns are placed in a linear order, given below in ascending order:
|
This comment has been minimized.
This comment has been minimized.
| /// ``` | ||
| #[unstable(feature = "float_total_cmp", issue = "88888888")] | ||
| #[inline] | ||
| pub fn total_cmp(self, other: f32) -> cmp::Ordering { |
This comment has been minimized.
This comment has been minimized.
ollie27
Sep 4, 2018
Contributor
Could this function take references like Ord::cmp does? It would allow writing things like a.sort_by(f32::total_cmp).
This comment has been minimized.
This comment has been minimized.
scottmcm
Sep 6, 2018
Member
I contemplated that, but decided that would be inconsistent with all the other things that would take references if f32 wasn't copy, but takes owned because it is copy. I note that there's currently not a single &self or &f32 in the inherent impl methods today.
Hopefully the copy type ergonomics stuff and better fn type coercions will make .sort_by(f32::total_cmp) just work sometime in the future.
This comment has been minimized.
This comment has been minimized.
gnzlbg
Oct 25, 2018
Contributor
I was wondering about this too. If one just can't .sort_by(f32::total_cmp) people will definitely reach to unsafe code for going from &[f32]s to &[Total<f32>] and vice-versa.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
The doc comment should mention somewhere prominent that despite its lengthy definition, this is implemented really efficiently with bit twiddling and integer comparisons, to encourage people to use it. I was thinking of combining this with a one sentence summary of the intent of the method (a total order for floats that is sensible but also orders NaNs). Currently we just have a reference to the standard's |
This comment has been minimized.
This comment has been minimized.
|
r? @rkruppe |
This comment has been minimized.
This comment has been minimized.
|
Is there a reason why we're not relying on LLVM to compare floats rather than fiddling with bitwise operations? Is it perhaps inconsistent with the exact ordering we'd like to guarantee? I'm curious because LLVM can probably emit more efficient instructions for comparing floats than whatever we do in Rust. |
This comment has been minimized.
This comment has been minimized.
|
Another question: Why are we introducing a new method rather than introducing a wrapper type? We already have something similar in the form of Wrapper types also make sorting floats easy: There's another similar wrapper type: It seems to me wrapper types already became the established pattern for that kind of thing. Is there something I'm missing? |
This comment has been minimized.
This comment has been minimized.
As far as I know LLVM does not provide this operation. The Edit: I also don't know of any hardware that provides dedicated support for
I don't have an answer to that. I don't really have an opinion on the API either, I'll leave that to libs folks. |
Mark-Simulacrum
assigned
alexcrichton
and unassigned
Mark-Simulacrum
Sep 12, 2018
scottmcm
referenced this pull request
Sep 14, 2018
Closed
Debug-print negative not-a-number as "-NaN" #54235
scottmcm
force-pushed the
scottmcm:float-total-cmp
branch
from
8040b8d
to
d0331bd
Sep 15, 2018
This comment has been minimized.
This comment has been minimized.
Because I started with I tried a If there is a type, that means a naming and wrapper-vs-concrete discussions, like the
This doesn't work for the same reason that |
This comment has been minimized.
This comment has been minimized.
|
FWIW although I'm listed as the reviewer here I'm happy to defer to others in this thread as y'all sound more knowledgeable than I anyway! |
This comment has been minimized.
This comment has been minimized.
|
I'd say r=me wrt the implementation if it gains some comments explaining why the bit fiddling works (we had good answers to that in a discussion GH now marked as "outdated"), but I have no opinion on the open question of how to expose this functionality to users -- newtype, inherent method(s), etc. Feedback from @rust-lang/libs would be welcome. |
This comment has been minimized.
This comment has been minimized.
So #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Hash)]
#[repr(transparent)]
pub struct Wrapping<T>(pub T);Type There is no We'd probably define it like this: #[derive(Clone, Copy, Default, Hash)]
#[repr(transparent)]
pub struct OrdFloat<T>(pub T);And then we'd add impls of |
TimNN
added
S-waiting-on-author
and removed
S-waiting-on-review
labels
Oct 16, 2018
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Ping from triage @scottmcm: What are your plans for this PR? |
This comment has been minimized.
This comment has been minimized.
jpetkau
commented
Oct 24, 2018
scottmcm
force-pushed the
scottmcm:float-total-cmp
branch
from
72c5e09
to
f2b9b53
Oct 25, 2018
This comment has been minimized.
This comment has been minimized.
|
Ok, I put in a newtype in for this. Let me know what you think. I left the method largely as a place to put documentation about the order; the examples look terrible using the newtype everywhere. |
| @@ -462,6 +462,40 @@ impl<T: Ord> Ord for Reverse<T> { | |||
| } | |||
| } | |||
|
|
|||
| /// A wrapper newtype for providing a total order on a type that's normally partial. | |||
This comment has been minimized.
This comment has been minimized.
gnzlbg
Oct 25, 2018
Contributor
The brief summary seems to indicate that only T: PartialOrds are accepted but the signature does not require that.
This comment has been minimized.
This comment has been minimized.
scottmcm
Oct 27, 2018
Member
That's completely true, but note that cmp::Reverse also doesn't bound itself.
This comment has been minimized.
This comment has been minimized.
gnzlbg
Oct 29, 2018
•
Contributor
Indeed, do you know why that is? Appears to be an oversight, what use is reverse if T does not implement PartialOrd ? - If this was an oversight for Reverse, we don't have to make the same mistake here.
| #[derive(Debug, Copy, Clone)] | ||
| #[unstable(feature = "float_total_cmp", issue = "55339")] | ||
| pub struct Total<T>(#[unstable(feature = "float_total_cmp", issue = "55339")] pub T); | ||
|
|
This comment has been minimized.
This comment has been minimized.
gnzlbg
Oct 25, 2018
•
Contributor
The PartialOrd/Ord "API" makes the assumption that only a single three-way comparator makes sense for any given type. This API covers the case in which more than one comparator makes sense for a given type, and the one implemented for the type is not total.
This feel like an extremely niche case for such a general API in core::cmp. For example, are we going to cover the opposite case, e.g., by adding Partial<T> as well? What about types with two total orders, where users would like to use the non-default one: are we going to add a Total2<T> to core::cmp to cover that? I don't think adding these kinds of types to core::cmp are a good solution to the problem.
Floats are ubiquitous, and it makes sense to make them easier to use. I think we could add [f32, f64]::total_order(self) -> TotalFloat<Self> methods to the floating-point types, and a TotalFloat type wrapper to core::float (or somewhere else) that implements the float API, but where the default ordering is total instead of partial. That's a smaller and more focused problem to solve.
In std::cmp, either we should try to solve the underlying problem, or provide better duct-tape, e.g., Total<T, Fn(Self, Self)-> Ordering> and Partial<T, Fn(Self,Self)->Option<Ordering>>, such that one can write Total<f32, f32::total_cmp> to select the "default" orderings or similar.
The PartialOrd/Ord provide "the default ordering for a type", but the thing they do not address yet is that partial / total are properties of orderings, not of types, and that multiple orderings with different properties exist for most types.
A less intrusive solution to the problem of adjusting the default ordering of aggregates could be to just use macros to control which ordering is used, e.g., instead of
#[derive(PartialOrd,Ord,PartialEq,Eq)]
struct A {
x: Total<f32>,
}one could
#[derive(PartialOrd,Ord,PartialEq,Eq)]
struct A {
#[Ord = f32::total_cmp, Eq = f32::total_eq]
x: f32,
}A better long-term solution for types with multiple orderings in the ecosystem would be to evolve APIs like HashSet from requiring the constraints on the type HashSet<T: Eq + Hash>, which kind of implies that for every T there is only an Eq and Hash impl that makes sense, to a de-coupled API like HashSet<T, O: Eq<T> = <T as Eq> where T: Eq, H: Hash<T> = <T as Hash> where T: Hash> that lets the user decide which ordering / hashing to use when it needs to, while falling back to a default if none are provided.
| /// ``` | ||
| #[unstable(feature = "float_total_cmp", issue = "55339")] | ||
| #[inline] | ||
| pub fn total_cmp(self, other: f64) -> cmp::Ordering { |
This comment has been minimized.
This comment has been minimized.
gnzlbg
Oct 25, 2018
Contributor
Could these be implemented using a macro so that all tests and docs can be shared?
This comment has been minimized.
This comment has been minimized.
scottmcm
Oct 27, 2018
Member
Plausibly, but that's a general f32.rs/f64.rs problem not specific to this method -- the same is true of min, max, to_bits, etc -- so I will not be taking action on it in this PR.
This comment has been minimized.
This comment has been minimized.
gnzlbg
Oct 29, 2018
•
Contributor
Just because the other methods could be improved does not mean that we have to make the situation ""worse"" with this PR. It would be better to just add a float_total_ord.rs module with a macro to define the method and the tests only once, and use that from f32.rs/f64.rs.
If someone wants to reduce duplication for the other methods, they can do the same, and once we have a couple of files we could move them all to a sub-directory or something, but we should start somewhere.
gnzlbg
suggested changes
Oct 29, 2018
I'm uncomfortable with adding the Total<T> API to std::cmp. I feel that it has not been discussed enough / achieved enough consensus, and would prefer if that would be split into a separate PR or a pre-RFC-like discussion in internals since it is unnecessarily blocking the addition of the total_cmp methods.
The total_cmp methods are very useful even without a Total<T> API (worst case users can write their own Total<T>-like API in their own crates, so not having that right now is not the end of the world). I (and it appears that others) are unsure about the total_cmp API, e.g., because it cannot be used for sort_by(f32::total_cmp) "as is", but nobody has proposed anything better and the current API is already good enough so I think it should be merged as is.
I raised some nitpicks about code duplication, but I leave it up to @scottmcm whether these are to be resolved.
TimNN
added
S-waiting-on-review
and removed
S-waiting-on-author
labels
Nov 6, 2018
This comment has been minimized.
This comment has been minimized.
|
Ping from triage @alexcrichton: It looks like this PR is now ready for your review. |
This comment has been minimized.
This comment has been minimized.
|
This sort of continues to have a very large amount of discussion which doesn't seem to have a clear conclusion. @scottmcm should this be closed until the discussion has been settled? |
This comment has been minimized.
This comment has been minimized.
|
Clearly this is going to take more energy than I have available for it. |
scottmcm commentedSep 4, 2018
Adds
fN::total_cmpfollowing the IEEEtotalOrderrules, inspired by a discussion with @rkruppe.