One more thing that occurred to me is that while partial equality is certainly a weird concept, and not many people would lose sleep over removing it, partial orderings are fairly common and removing those as an option doesn’t seem like the best idea.
This is not my impression. There may be many programs for which nobody ever notices a problem with fast-math, but there's also plenty of programs that deliberately don't use it, and more than enough horror stories of broken results from fast-math to ignore.
That's all good and well but these benefits could be had without fast-math, and infact there are already libraries providing no-NaN types. They should perhaps be more widely deployed, but coupling it to behavior that makes release mode behavior less predictable doesn't sound great.
Possibly, but it has to be worth the complexity increase. And again, I'm uneasy with bringing all these different concerns together into one single type. Which brings me to:
That may indicate that these concerns are not (all) important enough to deserve their own built-in type. That doesn't mean the status quo is perfect, there might be other solutions. For example, fast-math might also be made more convenient by introducing lexically scoped regions where the fast-math flags apply. (This won't affect called functions, but with a separate type, those functions would also have to be made generic to allow the caller to choose fast-math flags. Also note that such scopes have long been proposed for overriding overflow checking.)
Here I mean “undefined behavior” as defined by LLVM.
It’s not a colloquial “lacking definition”, but “precisely defined condition that allows LLVM to delete or miscompile any code proven to cause UB”.
(which of course should not be used here)
In LLVM terminology that’s implementation-defined behavior.
For me that’s the desired behavior I have a ton of finite-math code that LLVM doesn’t optimize well, because it works extra hard to preserve values and conditions that are not there.
I'm interested more in fast-math than Ord
. So from my perspective it's the other way: to use fast-math properly I can't allow NaN
s. If I banish NaNs, then my code becomes Ord
-compatible as a side effect.
I could have the side effect without the main goal, but I need the main goal
That would work for me too. However, a scope would require another mechanism for ensuring NaNs aren't present in the values used. Rust already has a precedent of using types to control overflow checking.
A bit of a false advertising in the original post, then. What you actually want is a way to use fast math, but PartialOrd
problems are orthogonal to that. Since I'm only interested in ergonomy of PartialOrd
here, and addition of new math doesn't help with that at all, I'm unsubsribing from this topic.
Anyone know which kinds of things in LLVM are gated by nnan
? That’d affect whether OrdF32
is fine as a library or would drastically benefit from compiler support…
Conditions that you believe are not there. Not statically proved.
Optimizing based on wrong assumptions is dangerous and undefined behavior. It cannot be implementation defined, because the implementation then would have to specify exactly what will happen. And it cannot do this since this can affect completely unrelated code.
Admittedly I do not know what LLVM calls an undefined behavior but the reference gives a list of behaviors considered undefined in Rust and among them you have:
Invalid values in primitive types, even in private fields/locals:
- Dangling/null references or boxes
- A value other than false (0) or true (1) in a bool
- A discriminant in an enum not included in the type definition
- A value in a char which is a surrogate or above char::MAX
- Non-UTF-8 byte sequences in a str
To me this looks a lot like what you are proposing.
This is no different from vec[i]
or a / b
. Correctness of either is an assumption and cannot be statically proved.
In my particular case all floats come from rgb / 255.0
, so I have sound reasons to believe they are finite. And I'm proposing debug assertions to confim that at run time.
@troplin @burakumin The terms "Undefined Behavior" and "Implementation Defined" come from the C spec and have been adopted by LLVM to describe how it optimizes certain code.
These terms may not fit your common-sense understanding of what is defined properly, but UB and ID are not an opinion, they are names for LLVM features. They could as well be called Blerg and Ulg. Fast math is not Blerg, but is Ulg. Division by zero is Blerg. High order bit of right shift is Ulg.
Fast math assumes values are finite and addition is commutative. If that doesn't suit your assumptions, then don't use the type that makes it a contract.
If you deliberately use NaN
, then use regular f32
. If you don't expect to have NaN
, but get one anyway, then it's a bug in your code.
That's no different than making assumptions that values in your program are positive and using u32
. If values may be negative, use i32
. Just like if you accidentally do 1u32 - 5u32
. That doesn't make u32
type dangerous.
These operations have garbage-in garbage-out behavior. If you feed them garbage, then it's not really a big deal that you might get a different garbage than the garbage that the other mode would produce. In such case you never had a chance to get valid value in the first place, because your code violated assumptions in your head.
The problem is not the expression itself, but further optimizations that depend on those assumptions.
Take for example a float -> integer cast. This is currently undefined behavior in Rust, which ist considered a bug.
Now let's assume that this bug was fixed by testing for INF and NaN before casting. If you use your new shiny finite types, the compiler is allowed to optimize those tests away and we're back to good old UB.
You cannot just argue on the LLVM level, without considering the semantics of Rust. Rust is not C.
Terms are defined relatively to a context. You can argue that this is not up to the Rust Reference to give an alternative meaning to "Undefined Behaviour" and that the source of truth on this topic is either the LLVM manual or the C spec but in that case we have a much bigger problem than optimizing floating-point types and you should instantaneously start a thread "The Rust manual is wrong!"
I don't understand how this comparison could be valid. AFAIK these operations panics when the requirements are not met (both in debug and release mode). Are there some flags to remove the checks ?
I understand your argument as “if we implement it wrong in Rust, it will be wrong in Rust”. If rustc inserts non-optional checks in the way that they’re understood as optional for LLVM, that’s just a rustc bug.
The as
operator generting an undef
is an interesting bug, but it’s just an implementation bug. It’s not an inherent unsafety of floats or LLVM, it’s a bug in rustc/LLVM integration that will be fixed.
That was just a counter-example for argument that Rust can't have conditions that can't be statically proven. These panic exactly because they could not have been statically proven.
For non-panicking example consider a+b
. Rust can't statically prove it won't overflow. It will panic in debug mode (same as I propose with float overflows causing Inf
s) and will quietly overflow in release mode (same as I propose with float overflows).
I really don’t understand why this discussion seems to have focused on “proper” handling of NaN
s in a type that where any NaN
by definition is complete garbage with no useful purpose whatsoever, and is not allowed to be used to produce any useful value in any case, by definition.
I think it’s not fair to want to deliberately break the contract of the type, feed invalid-by-definition values to an expression and then still expect it to produce valid values from invalid inputs.
I feel like if I proposed to have an integer-only type I’d have to defend its handling of 1.5
and how horrible it would be for programs using u32
not to be able to add 1.5 + 1.5
correctly.
If you ready my post carefully, you will see that my argument was based on a future rustc
where the bug is fixed.
You always argue with LLVM, but LLVM is just the implementation. I'm arguing on the language level: If your type defines that NaN and INF cannot happen, the any optimizer (not just LLVM) can optimize based on that fact. If you are producing an illegal bit pattern and use that value later on, that's undefined behavior, you cannot just deny that.
I think this is fair, since the contract is only based on convention. And it requires no unsafe code to break it. It's possible to produce such garbage values with perfectly legal inputs, using completely innocent operations.
C is such a language and I really don't want Rust (Rust, the language, not rustc
, the compiler) to make even a small step in the direction of allowing UB in safe code.
The big difference is, that an integer cannot represent 1.5
, while your type can represent NaN.
And operations on integer cannot produce 1.5
, while operations on your type can produce NaN.
This was considered in the run-up to 1.0, but in the end, we kept what we had/have.
Having followed the discussion so far, one avenue that doesn’t seem to have been followed is: what if this type was only allowed to exist in unsafe code? Then you would have to cast it to a regular float to use it in safe code, but if you have a hot path that deals with floating point values, you can opt in to the optimization at least.
Of course, that changes nothing for PartialOrd, and it still introduces some UB. Is UB allowed to exist in unsafe code as it is today?
Maybe our disagreement comes from the fact that I precisely don't agree with that statement. NaN is a valid value of floats with defined behaviour for mathematical operations so it is not complete garbage. It's part of of the floating-point arithmetic. We can of course discuss if the chosen behaviour was the most appropriate (with the infamous NaN != NaN) or if it should have been replaced by another mechanism (crashing the app/exception/mere undefined behaviours when for example computing 0.0/0.0). We can also complain about the fact it may be undesirable in many numeric applications. Now it is still a valid part of the arithmetic implemented by processors (to be clear I don't like it either) and as such it is not a second-class value.
Basically as I see NaN, it is nothing but an internal None
value for floating point types. Imagine that now the compiler considers some Option<>
are necessarily Some
variants without any static check and if a None
is unfortunately introduced, instead of panicking the program produces whatever behaviour LLVM fancied. I would personally not like it.