Skip to content

Our floating point semantics were a mess #237

Closed

Description

@RalfJung

Floating-point semantics are hard, in particular the NaN part, but this should describe them accurately -- except on x86-32, for more than one reason.

We still need to officially decide that those are our intended semantics though. rust-lang/rust#73328 tracks that on the rustc side.

Historic info

There are several ways in which our de-facto FP semantics currently are broken:

I'm adding this here because figuring out the semantics of Rust is part of the goal of the UCG (I think?) and we should have an overarching "FP semantics" tracker somewhere.

Activity

  1. added
    C-open-questionCategory: An open question that we should revisit
    A-floatsTopic: concerns floating point operations/representations
    on Jun 14, 2020
  2. workingjubilee commented on Sep 12, 2020

    @workingjubilee
    Member

    Worth noting: we can't expect floating point to be handled consistently on all architectures and over all datatypes that use floating points, yet it might be confusing if floating points have wildly different behavior based on the box they're in. rust-lang/rfcs#2977 (comment)

  3. scottmcm commented on Sep 12, 2020

    @scottmcm
    Member

    This also reminds me of rust-lang/rust#75786 -- We should probably have an explicit accuracy promise documented somewhere, including for library functions.

    (Hopefully we can at least promise ±1ULP everywhere, though some things really ought to be ±½ULP.)

  4. RalfJung commented on Sep 12, 2020

    @RalfJung
    MemberAuthor

    Worth noting: we can't expect floating point to be handled consistently on all architectures and over all datatypes that use floating points

    Usually, that's why specs can be under-specified -- they can leave room for implementations to differ, either via non-determinism (that's what wasm does for NaNs; would be interesting what they do for denormals) or via introducing unspecified target-specific "canonicalization" or similar mechanisms.

    This also reminds me of rust-lang/rust#75786 -- We should probably have an explicit accuracy promise documented somewhere, including for library functions.

    That's just trig (and other) functions being platform-specific (the division/multiplication in the title is a red herring). So it's related but the issue here is about how operations provided by the core language behave. We can worry about trig functions once we have that sorted out. ;)

  5. workingjubilee commented on Sep 20, 2020

    @workingjubilee
    Member

    Adding some background data: I recently discovered that Intel recommends setting the denormals-are-zero and flush-to-zero flags on their C++ compiler unless floating point denormal accuracy is application critical: https://software.intel.com/content/www/us/en/develop/documentation/cpp-compiler-developer-guide-and-reference/top/compiler-reference/floating-point-operations/understanding-floating-point-operations/setting-the-ftz-and-daz-flags.html

    In addition, the fact that a floating point op can vary in speed by 2 orders of magnitude is a rich source of data for timing attacks: https://cseweb.ucsd.edu/~dkohlbre/papers/subnormal.pdf

    Also, explicit fused multiply-add ops are part of the IEEE754-2008 standard and usually can lead to increased precision by reducing the number of roundings that occur... but that can also reduce the accuracy of later operations that encode assumptions like "this is commutative, right?"

  6. RalfJung commented on Sep 20, 2020

    @RalfJung
    MemberAuthor

    In addition, the fact that a floating point op can vary in speed by 2 orders of magnitude is a rich source of data for timing attacks: https://cseweb.ucsd.edu/~dkohlbre/papers/subnormal.pdf

    Speed is not currently part of the Rust specification, so that aspect, while certainly relevant in general, is unrelated to specifying floating-point behavior in Rust.

    Also, explicit fused multiply-add ops are part of the IEEE754-2008 standard and usually can lead to increased precision by reducing the number of roundings that occur... but that can also reduce the accuracy of later operations that encode assumptions like "this is commutative, right?"

    AFAIK LLVM does not introduce FMUs unless we explicitly set a flag, so right now I think there is no problem here. Unless you want to suggest we should introduce FMUs; that could complicate our floating-point story even further but in ways that are orthogonal to the problems listed here. There was at least one attempt at an RFC here (rust-lang/rfcs#2686), but since it is a hypothetical future (we don't introduce FMUs right now) I don't think it needs tracking here.

    Adding some background data: I recently discovered that Intel recommends setting the denormals-are-zero and flush-to-zero flags on their C++ compiler unless floating point denormal accuracy is application critical: https://software.intel.com/content/www/us/en/develop/documentation/cpp-compiler-developer-guide-and-reference/top/compiler-reference/floating-point-operations/understanding-floating-point-operations/setting-the-ftz-and-daz-flags.html

    What does this mean for Rust programs? This sounds a bit like a fast-math style flag? Similar to the optimizer introducing FMUs, to my knowledge the complications around that are orthogonal to figuring out what the Rust semantics for completely standard IEEE floating point operations are when NaNs or weird platforms (i686) are involved.

    Fast-math is its own can of worms, and very little is known about how to specify it precisely. In contrast, the issue here is mostly about figuring out what LLVM (and, by extension, hardware) actually does; writing a reasonable spec is easy ("copy wasm"), but making sure LLVM conforms to that spec is hard because, as usual, LLVM IR behavior is scarcely documented.

    Or is this related to the denormal problem around SSE that you raised earlier?

  7. Lokathor commented on Sep 20, 2020

    @Lokathor
    Contributor

    I think a separate issue is warranted for denormals and fast math issues, as long as a fix to our NaN and x87 issues don't block potential avenues there they're almost entirely separate.

  8. antoyo commented on Feb 19, 2022

    @antoyo

    Not sure where to post this comment, but rustc_codegen_gcc has a NaN with a different sign than Rust with LLVM.
    I'm wondering what to do about this, but I guess the solution has not been figured out.

  9. digama0 commented on Feb 20, 2022

    @digama0

    @thomcc wrote a lengthy post about this in https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/float.20total_cmp/near/270506799, and the surrounding conversation is also relevant.

  10. workingjubilee commented on Feb 20, 2022

    @workingjubilee
    Member

    I believe I have devised a path forward which may allow us to cut out the i686 issue, by simply having Rust functions use the calling convention that LLVM annotates them with after optimizations and guaranteeing SSE2 is enabled and x87 is disabled outside extern "C" and the like.

  11. 32 remaining items

  12. RalfJung commented on Dec 28, 2022

    @RalfJung
    MemberAuthor

    It is surprising if const evaluation produces results which are unobtainable in a runtime evaluation.

    That's exactly what is being discussed in rust-lang/rfcs#3352. I agree it is surprising but I think it is better than the alternatives. (Your statement boils down to "runtime behavior must be superset of compiletime behavior", which is being discussed in the RFC thread, though not yet in the RFC text.) So let's discuss that question there and not here.

    Also note that all your statements assume that Rust float semantics are exactly IEEE float semantics, which is not a given. We eventually might adopt something like the wasm semantics which makes more guarantees than IEEE when it comes to NaNs.

    In that regard, it cannot be soundly constant-folded without knowing the details of the target (if the ISA even specifies which NaNs it produces in that instance).

    This statement is in fact wrong if Rust uses IEEE float semantics. Since under those semantics NaNs bits are picked non-deterministically when the NaN is produced, Rust can constant-fold such operations to an arbitrary NaN.

    IOW, Rust does not guarantee that NaNs behave the same way as they would if you were to write assembly by hand. It guarantees that they behave as in a valid IEEE implementation (and maybe even a particular variant of that, as in the wasm case), but it doesn't have to be the same IEEE implementation as what your hardware does.

  13. Muon commented on Dec 28, 2022

    @Muon

    Also note that all your statements assume that Rust float semantics are exactly IEEE float semantics, which is not a given. We eventually might adopt something like the wasm semantics which makes more guarantees than IEEE when it comes to NaNs.

    I am indeed expecting that Rust uses IEEE 754 semantics. After all, it's what almost all hardware implements, and that's fast.

    This statement is in fact wrong if Rust uses IEEE float semantics. Since under those semantics NaNs bits are picked non-deterministically when the NaN is produced, Rust can constant-fold such operations to an arbitrary NaN.

    An arbitrary quiet NaN, specifically. However, if we're using the hardware FPU, we don't determine which NaNs are quiet, so the result is still target-dependent.

  14. RalfJung commented on Dec 28, 2022

    @RalfJung
    MemberAuthor
  15. Muon commented on Dec 29, 2022

    @Muon

    Ah, I see. So, exactly matching the spec is in a certain sense ill-posed. The spec requires that both quiet and signaling NaNs exist and that computational operations only return quiet NaNs. That is, there is more than just nondeterminism at play here. For a (nondeterministic) abstract machine to comply, it must still at minimum choose some NaNs to be quiet and some to be signaling. Notably, different machines can make different choices. However, if necessary, Rust can leave the exact bit patterns unspecified.

    Implementation-wise, there are a few pitfalls with actually achieving compliance. Firstly, at some point, rustc has to output specific bits for NaNs, and there are real-world CPUs which disagree about signaling and quiet NaNs. Notoriously, MIPS does the opposite of x86 and ARM. Secondly, LLVM is still probably far away from spec compliance, even disregarding NaNs (llvm/llvm-project#44497, llvm/llvm-project#43070, llvm/llvm-project#24913, llvm/llvm-project#25233, llvm/llvm-project#18362).

  16. RalfJung commented on Jan 2, 2023

    @RalfJung
    MemberAuthor

    TBH my inclination is to entirely ignore everything related to signalling NaNs for now. AFAIK that is what LLVM does, so some groundwork will be needed elsewhere before we can even attempt to do any better.

  17. Muon commented on Jan 3, 2023

    @Muon

    That's somewhat tenable, I think? We don't currently have any way of distinguishing sNaN from qNaN. The ordering given by total_cmp (our version of the totalOrder predicate) is based on the standard's recommendation for what should be a signaling NaN, not the underlying implementation. The documentation they may need to be adjusted. We'd also need to document that our operations are not strictly compliant and can in fact return any NaN pattern. That said, I'm not sure this is simpler than saying we return unspecified qNaNs and leaving unspecified which patterns are qNaNs/sNaNs.

  18. RalfJung commented on Jul 18, 2023

    @RalfJung
    MemberAuthor

    I have written a pre-RFC to document our current floating-point semantics. Please comment on Zulip or hackmd!

  19. Muon commented on Jul 18, 2023

    @Muon

    I'll have a look through and comment on it when I get back either tomorrow or when I get back home in a few days.

  20. RalfJung commented on Aug 4, 2023

    @RalfJung
    MemberAuthor

    So... I think this is not so much of a mess any more?

    For the x86-32-specific trouble, the FCP in rust-lang/rust#113053 went through. These are unambiguously platform-specific bugs but are hard to fix. We now have rust-lang/rust#114479 and rust-lang/rust#115567 tracking this.

    Most of the rest boils down to "we don't guarantee much about the bits you get out of a NaN-producing operation". That is tracked in rust-lang/rust#73328. My Pre-RFC describes our current de-facto guarantees.

    I assume that RFC will need approval by t-opsem and t-lang. Let's keep an issue open on our side as well until we have a team decision on this.

  21. changed the title [-]Our floating point semantics are a mess[/-] [+]Our floating point semantics were a mess[/+] on Aug 4, 2023
  22. added a commit that references this issue on Oct 3, 2023
  23. added a commit that references this issue on Oct 3, 2023
  24. RalfJung commented on Oct 14, 2023

    @RalfJung
    MemberAuthor

    The RFC rust-lang/rfcs#3514 should resolve all questions for good. :)

  25. RalfJung commented on Jul 29, 2024

    @RalfJung
    MemberAuthor

    Superseded by rust-lang/rust#128288, now that the RFC got accepted!

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-floatsTopic: concerns floating point operations/representationsC-open-questionCategory: An open question that we should revisit

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions