Skip to content

Document guarantees (or lack thereof) regarding sign, quietness, and payload of NaNs #73328

Closed

Description

@ecstatic-morse

NaNs can behave in surprising ways. On top of that, a very common target is inherently buggy in more than one way. But on all other targets we actually follow fairly clear, if improperly documented, rules. See here for the current status.

Original issue

Several issues have been filed about surprising behavior of NaNs.

The root cause of these issues is that LLVM does not guarantee that NaN payload bits are preserved. Empirically, this applies to the signaling/quiet bit as well as (surprisingly) the sign bit. At least one LLVM developer seems open to changing this, although doing so may not be easy.

Unless we are prepared to guarantee more, we should do a better job of documenting that, besides having all 1s in the exponent and a non-zero significand, the bitwise value of a NaN is unspecified and may change at any point during program execution. In particular, the from_bits method on f32 and f64 types currently states:

This is currently identical to transmute::<u32, f32>(v) on all platforms.

and

this implementation favors preserving the exact bits. This means that any payloads encoded in NaNs will be preserved

These statements are misleading and should be changed.

We may also want to add documentation to {f32,f64}::NAN to this effect, see #52897 (comment).

cc #10186?

Activity

  1. added
    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.
    T-langRelevant to the language team
    on Jun 13, 2020
  2. changed the title [-]Document guarantees (or lack thereof) regarding signedness, quietness, and payload of `NaN`s[/-] [+]Document guarantees (or lack thereof) regarding sign, quietness, and payload of `NaN`s[/+] on Jun 13, 2020
  3. ecstatic-morse commented on Jun 13, 2020

    @ecstatic-morse
    ContributorAuthor

    This also affects the documentation for the methods in #72568.

  4. RalfJung commented on Jun 14, 2020

    @RalfJung
  5. ecstatic-morse commented on Jun 14, 2020

    @ecstatic-morse
    Author
  6. RalfJung commented on Jun 14, 2020

    @RalfJung
  7. RalfJung commented on Jun 14, 2020

    @RalfJung
  8. RalfJung commented on Jun 16, 2020

    @RalfJung
    Member
  9. thomcc commented on Sep 9, 2020

    @thomcc
    Member

    Unless we are prepared to guarantee more, ... the bitwise value of a NaN is unspecified and may change at any point during program execution

    This seems... way too conservative. I know it's trying to make the best of a bad situation, and I'm sympathetic here, but please realize how hard overly broad unspecified behavior like this makes it to write robust code (As a user of Rust who came to it from C, this feels like the same kind of undefined behavior you see in the C standard in cases where all supported platforms disagree).

    So, my biggest concern is non-Wasm platforms. I think it would really be a huge blow to working with floats in rust to effectively zero guarantees around NaN. I don't really know a good solution here, but even just marking it as a LLVM bug on the problematic platforms (rather than deciding that this isn't a thing that Rust code gets to rely on ever) would be much better.

    Just as an example, if NaN payload is totally unspecified and may change at any point, implementing any ordering stronger than PartialEq for floats is impossible (including #72599), as you cannot count on NaN bitwise values to be stable across two calls of to_bits() on the same float.

    Same goes for things that stash f32 in a u32 and then expect to get it out again and be the same (for example, I implemented an AtomicF32 at one point on top of AtomicU32 + from_bits/to_bits. If I can't rely on stable bit values though from float => u32, things like compare_exhcange loops become not guaranteed to ever terminate.


    Tbat said, I also "totally unspecified behavior" is too conservative on Wasm too — I've done a bit of poking and it seems like the behavior is a lot more sane than suggested, although it does violate IEEE734 and is probably not 100% intentional.

    Basically: LLVM's behavior here is inherited from the wasm/js runtime, which canonicalizes NaNs whenever going from bits => float, as it wants to be able to guarantee certain things about which bit patterns are possibly in the float — certain NaNs are off limits.

    That means:

    • The bits=>float operation is the only time the NaN payload can change (explaining the mentioned f32::from_bits(x).to_bits() round trip failure
    • Float => bits should be totally stable and consistent
    • After a float => bits operation, those bits are guaranteed not to change when going back to a float.
      • There is, admittedly, some dodginess here since perhaps LLVM optimizes a bits => float => bits into a no-op. Perhaps that can be addressed directly and more easily though?

    This is non-ideal but is still way easier to reason about and build on top of than arbitrary unspecified behavior.


    Yeah that's the basic gist of my thoughts. Changing the documented guaranteed of from_bits/to_bits globally like that would totally neuter those APIs. I'm sympathetic to the position you're in and not having great choices, but that kind of change feels like very much the wrong call, and making the call be this kind of unspecified behavior feels really bad on any platform...

    P.S. I accidentally posted an incomplete version of this comment by hitting ctrl+enter in the github text box, sorry if you saw that — really should just do these in a text editor first.

  10. RalfJung commented on Sep 9, 2020

    @RalfJung
    Member

    I am open to better suggestions. I know hardly anything about floating point semantics, so "totally unspecified" is an easy and obviously "correct" choice for me to reach for. If someone with more in-depth knowledge can produce a spec that is consistent with LLVM behavior, I am sure this can be improved upon.

    However, the core spec of Rust must be platform-independent, so unless we consider this a platform bug (which I think is what we do with the x87-induced issues on i686), whatever the spec is has to encompass all platforms.

    In principle, certain platforms can decide to guarantee more than others, but that is a dangerous game as it risks code inadvertently becoming non-portable in the worst possible way -- usually "non-portable" means "fails to build on other platforms", now it would silently change behavior. Maybe we can handle this in a way similar to endianess, although the situation feels different.

    And all of this is assuming that we can get LLVM to commit to preserving NaN payloads on these platforms. You are saying that this issue only affects wasm(-like) targets, but is there a document where LLVM otherwise makes stronger guarantees? The fact that issues only have been obvserved on these platforms does not help, we need an explicit statement by LLVM to establish and maintain this guarantee in the future.

    Just as an example, if NaN payload is totally unspecified and may change at any point, implementing any ordering stronger than PartialEq for floats is impossible (including #72599), as you cannot count on NaN bitwise values to be stable across two calls of to_bits() on the same float.

    So if I understand correctly, on wasm, the float => bit cast that is inherent in such a total order would canonicalize NaNs. This on its own is not a problem as this is a stable canonicalization, and that's why you think "unstable NaNs" are too broad. Is that accurate?

    However, when you combine that with LLVM optimizing away "bit => float => bit" roundtrips (does it do that?), then this already brings us into an unstable situation. Some of the comparisons might have that optimization applied to them, and others not, so suddenly the same float (obtained via a bit => float cast) can compare in two different ways.

    It is easy to make a target language spec such as wasm self-consistent, but to do the same on a heavily optimized IR like LLVM's or surface language like Rust is much harder.

  11. thomcc commented on Sep 9, 2020

    @thomcc
    Member

    So if I understand correctly, on wasm, the float => bit cast that is inherent in such a total order would canonicalize NaNs.

    No, float => bit should always* be stable, it's bit => float that canonicalizes. This means it's possible to implement a robust totalOrder without issues on Wasm (just not if all nan payloads are unspecified values which may change at any time).

    My point with that paragraph was not that the LLVM behavior is bad (although I am not a fan), but that changing Rust's guarantees to: "the bitwise value of a NaN is unspecified and may change at any point during program execution" is both

    • Stronger than needed for Wasm
    • Makes it so that no matter which operations happen to canonicalize and which do not, it's not possible to write a totalOrder.

    * (always... except for what I say in my next response)


    However, when you combine that with LLVM optimizing away "bit => float => bit" round-trips (does it do that?)

    I don't know if it does it on Wasm, but it's obviously free to do this on non-Wasm platforms (and I think I've seen it there, but it's hard to say and I don't have code I'm thinking of on hand).

    I'd hope it wouldn't do this on Wasm, and would argue that if it does optimize that away it's an LLVM bug for that platform, but... yeah. Possible.


    unless we consider this a platform bug (which I think is what we do with the x87-induced issues on i686)

    Honestly that seems like the sanest decision to me, since the alternative is essentially saying that Rust code can't expect IEEE754-compliant floats anymore. And so, I think x87 is a good example because it's also an example of non-IEEE754 compliance, although probably a less annoying one in practice.

    Concretely, I wouldn't have complained about this at all if it were listed as a platform bug.


    Instead, my issue is entirely with all compliant Rust code loosing the ability to reason about float binary layout, which has been extremely useful in stuff like scientific computing, game development, programming language runtimes, math libraries, ... All things Rust is well suited to do, by design.

    This wouldn't cripple those by any means, but it would make things worse for several of them.

    Admittedly, in practice, unless it's flat out UB, I suspect people will just code to their target and not to the spec, which isn't great either, but honestly to me it feels like it might be better than Rust genuinely inheriting this limitation from the web platform.

    (Ironically, this would also prevent writing a runtime in Rust that does the optimization which is the reason Wasm and JS runtimes want to canonicalize their NaNs. Although that optimization was already fairly unportable anyway)

  12. 24 remaining items

  13. RalfJung commented on Mar 20, 2021

    @RalfJung
    Member

    I believe this is due to one of these being impacted by LLVM's constant propagation and the other not.

    Sure. But that doesn't change the fact that this is runtime code. And to my knowledge, LLVM doesn't consider this optimization a bug, since the result produced by LLVM is legal according to the IEEE floating-point spec. There isn't even an LLVM bugreport for the f64::NEG_INFINITY * 0.0 case, is there?

    That said, IMO the solution to hard, low-impact bugs shouldn't be to rework the language so that they're not bugs. Eventually they should be fixed, even if it's not a high priority.

    It is my understanding that some aspects of the bitwise results of floating-point operations (in particular for NaNs) are inherently not defined in the LLVM IR semantics (or in the IEEE semantics, which LLVM [mostly?] follows). This is not a bug, it is part of their spec. So if we want to use LLVM as the backend, we have no choice but to also incorporate a similar kind of non-determinism into the Rust semantics (or lobby for LLVM to change their spec).

    This is not reworking the language, it is properly understanding the consequences of what it means to say that Rust uses IEEE floating-point semantics. If agree that it would be nice to have deterministic floating-point operations, but that's just not realistic when LLVM (and WebAssembly) made a different choice.

  14. RalfJung commented on Mar 20, 2021

    @RalfJung
    Member

    Put differently: a bug usually means that something is not working according to spec. I don't see that happen here (but I keep getting lost in the details of FP semantics). My understanding is that this issue is about better documenting the Rust spec, not about changing the behavior of rustc.

    One could argue that the spec has a bug due to being too liberal, but given that the spec we are talking about here is the LLVM IR spec and by extension the IEEE FP spec, that does not seem like a particularly useful of constructive approach. (Specs can certainly have bugs when they fail to be self-consistent or when they do not adequately reflect intended behavior, but that does not seem to be the case here.)

  15. workingjubilee commented on Mar 23, 2021

    @workingjubilee
    Member

    I do not believe lobbying LLVM for a hardware-respecting behavior seems that unlikely. It may make some proofs regarding optimizations easier, for one.

  16. RalfJung commented on Mar 23, 2021

    @RalfJung
    Member

    It may make some proofs regarding optimizations easier, for one.

    I don't see how that would be the case.

    I do not believe lobbying LLVM for a hardware-respecting behavior seems that unlikely.

    Fair. But this is the wrong forum to do so. ;)

  17. DemiMarie commented on Oct 29, 2021

    @DemiMarie
    Contributor

    What about refusing to constant-evaluate any operation that is non-reproducible?

  18. RalfJung commented on Oct 29, 2021

    @RalfJung
    Member

    By const-evaluate I assume you mean constant propagation / constant folding, i.e., the optimization pass that tries to avoid redundant computations at runtime? That is distinct from CTFE (compile-time function evaluation, also sometimes called const evaluation), which is about computations that the spec says happen at compile-time (such as the initial values of a const, array sizes, or enum discriminant values).

    We could do that in rustc, but can we convince LLVM to stop folding f64::NEG_INFINITY * 0.0?

  19. DemiMarie commented on Oct 30, 2021

    @DemiMarie
    Contributor

    By const-evaluate I assume you mean constant propagation / constant folding, i.e., the optimization pass that tries to avoid redundant computations at runtime? That is distinct from CTFE (compile-time function evaluation, also sometimes called const evaluation), which is about computations that the spec says happen at compile-time (such as the initial values of a const, array sizes, or enum discriminant values).

    We could do that in rustc, but can we convince LLVM to stop folding f64::NEG_INFINITY * 0.0?

    File a bug against LLVM? I don’t know 🙂

  20. added a commit that references this issue on May 9, 2022
  21. RalfJung commented on Aug 4, 2023

    @RalfJung
    Member

    I have written a Pre-RFC on our floating-point guarantees, which is almost exclusively about NaNs. That document describes what are currently the best possible guarantees we can provide, given LLVM's documentation. However, LLVM also seems to be open to providing stronger guarantees.

  22. the8472 commented on Sep 5, 2023

    @the8472
    Member

    and the last x86 processor without SSE2 left the plant about 20 years ago

    To be pedantic, the Vortex86DX3 is still being made and only supports SSE
    And they claim linux support. Some poor soul out there may still be compiling x86-no-SSE2 code for linux shipped on "new" hardware. That said, I'm not aware of any instances of this actually happening, just raising the possibility.

    Edit: #35045 (comment) mentioned in 2016 that he's using a VortexX86

  23. RalfJung commented on Sep 5, 2023

    @RalfJung
    Member

    I'm more concerned about someone using -C target-cpu=pentium on one of our tier 1 i686 targets an expecting that to work properly. Maybe we should just forbid disabling SSE2 support...

  24. RalfJung commented on Oct 14, 2023

    @RalfJung
    Member

    The RFC rust-lang/rfcs#3514 makes a concrete proposal for our guarantees for the bits of NaNs.

  25. RalfJung commented on Oct 5, 2024

    @RalfJung
    Member

    I think this was resolved by #129559.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.A-floating-pointArea: Floating point numbers and arithmeticT-langRelevant to the language team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions