Skip to content

Tracking issue: 32bit x86 targets without SSE2 have unsound floating point behavior #114479

Open

Description

@RalfJung

View all comments

On x86 (32bit) targets that cannot use SSE2 instructions (this includes the tier 1 i686 targets with flags that disable SSE2 support, such as -C target-cpu=pentium), floating-point operation can return results that are rounded in different ways than they should, and results can be "inconsistent": depending on whether const-propagation happened, the same computation can produce different results, leading to a program that seemingly contradicts itself (which can lead to miscompilations). This is caused by using x87 instructions to perform floating-point arithmetic, which do not accurately implement IEEE floating-point semantics (not with the right precision, anyway). The test tests/ui/numbers-arithmetic/issue-105626.rs has an example of such a problem.

Worse, LLVM can use x87 register to store values it thinks are floats, which resets the signaling bit and thus alters the value -- leading to miscompilations.

This is an LLVM bug: rustc is generating LLVM IR with the intended semantics, but LLVM does not compile that code in the way that the LLVM LangRef describes. This is a known and long-standing problem, and very hard to fix. The affected targets are so niche these days that that is nobody's priority. Note that this problem affects all binaries built without SSE2 being globally enabled, even if these binaries are later used on systems that do have SSE2 -- the only way to avoid the problem is to not use the x87 FPU at all, i.e., generate binaries that require SSE2. The purpose of this issue mostly is to document its existence and to give it a URL that can be referenced.

Some ideas that have been floated for fixing this problem:

  • We could emit different instruction sequences for floating-point operations that emulate the expected rounding behavior using x87 instructions. This will likely require changes deep in LLVM's x86 backend. This is what Java does.
  • We could use softfloats.
  • We could set the FPU control register to 64bit precision for Rust programs, and require other code to set the register in that way before calling into a Rust library. this does not work

Related issues:

Prior issues:

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Aug 4, 2023
  2. added
    C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFC
    A-floating-pointArea: Floating point numbers and arithmetic
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Aug 4, 2023
  3. DemiMarie commented on Aug 4, 2023

    @DemiMarie
    Contributor

    Can we just drop support for x86 without SSE2 or fall back to software floating point?

  4. RalfJung commented on Aug 4, 2023

    @RalfJung
    MemberAuthor

    Can we just drop support for x86 without SSE2

    We currently have the following targets in that category:

    • i586-pc-windows-msvc
    • i586-unknown-linux-gnu
    • i586-unknown-linux-musl

    They are all tier 2. I assume people added them for a reason so I doubt they will be happy about having them dropped. Not sure to what extent floating point support is needed on those targets, but I don't think we have a concept of "target without FP support".

    or fall back to software floating point?

    In theory of course we can, not sure if that would be any easier than following the Java approach.

  5. programmerjake commented on Aug 4, 2023

    @programmerjake
    Member

    or fall back to software floating point?

    In theory of course we can, not sure if that would be any easier than following the Java approach.

    it should be much easier since LLVM already supports that, unlike the Java scheme (afaik)

    https://gcc.godbolt.org/z/14WdnKhhP

  6. RalfJung commented on Aug 14, 2023

    @RalfJung
    MemberAuthor

    FWIW f32 on x86-32-noSSE should actually be fine, since double-rounding is okay as long as the precision gap between the two modes is big enough. Only f64 has a problem since it is "too close" to the 80bit-precision of the x87 FPU.

    On another note, we even have code in the standard library that temporarily alters the x87 FPU control word to ensure exact 64bit precision...

  7. changed the title [-]Tracking issue: 32bit x86 targets have non-compliant floating point behavior[/-] [+]Tracking issue: 32bit x86 targets without SSE have non-compliant floating point behavior[/+] on Sep 5, 2023
  8. changed the title [-]Tracking issue: 32bit x86 targets without SSE have non-compliant floating point behavior[/-] [+]Tracking issue: 32bit x86 targets without SSE2 have non-compliant floating point behavior[/+] on Sep 5, 2023
  9. RalfJung commented on Sep 5, 2023

    @RalfJung
    MemberAuthor

    I wonder if there's something that could be done about the fact that this also affects tier 1 targets with custom (stable) flags such as -C target-cpu=pentium. Do we still want to consider that a tier 1 target in itself? Is there a way that we can just reject flags that would disable SSE2 support, and tell people to use a different target instead?

  10. DemiMarie commented on Sep 5, 2023

    @DemiMarie
    Contributor

    @RalfJung What about forcing non-SSE2 targets to software floating point? That must be supported anyway because of kernel code.

  11. RalfJung commented on Sep 5, 2023

    @RalfJung
    MemberAuthor

    Yeah that's an option listed above, since you already proposed it before. I have no idea how feasible it is. People are very concerned about the softfloat support for f16/f128 leading to code bloat and whatnot, so the same concerns would likely also apply here.

    AFAIK the kernel code just doesn't use floats, I don't think they have softfloats?

  12. added a commit that references this issue on Oct 3, 2023
  13. 85 remaining items

  14. LekKit commented on Jan 7, 2026

    @LekKit

    I have stumbled upon a similar issue in my project, albeit not specifically related to Rust or LLVM, and have (somewhat) solved it.

    My emulator uses native host FPU to emulate conforming RISC-V floating point unit, yet it was subject to same problems with 8087 FPU discussed here (Signaling NaN / NaN payloads not preserved between 8087 loads/stores, excess intermediate precision). There were other issues which i also solved, but they are out of scope here.

    I solved it by not even using native FP types around the codebase, replacing them with 32/64 bit integer typedefs which carry the IEEE754 representation. They are freely passed around as integers on compiler IR / hardware level.

    Only around actual floating-point computations I bitcast the integer-typedef into a native floating-point type. That is fine, as IEEE754 doesn't guarantee preserving NaN payload across an operation. The compilers (both GCC and Clang) are smart enough to recognize where to put an fld/fst or integer mov in almost every case, optimizing away excess bitcasts, plus I get correctly rounded result (to f32 or f64) after each operation.

    Sure, this is somewhat slower than "naive" 8087 usage, but what matters is it was fully correct for my usecase, preserves passing around arbitrary NaN payloads, and is in fact orders of magnitude faster than softfloat.
    So it might be worth considering this idea either on LLVM side, or generate "floats as integer types" from Rust to LLVM IR on i686, bitcasting whenever needed around actual computations.

    One issue this does not solve, is returning arbitrary NaN payloads via 8087 ABI return. But maybe rust-specific ABI change for i686 could fix that, leaving only Rust<->C calls affected.

  15. RalfJung commented on Jan 7, 2026

    @RalfJung
    MemberAuthor

    We still have to use float types for extern "C" function signatures to ensure the right ABI is used. Also, LLVM will probably optimize away bitcasts when writing a series of FP operations. In an interpreter, LLVM can't know what the actual program is so there's not much to optimize; this changes quite a bit when writing more concrete code. So, while this helps for some situations, it's far from a full fix.

  16. programmerjake commented on Jan 7, 2026

    @programmerjake
    Member

    just using bitcasting from/to ints doesn't give correctly-rounded results for f64 with the 8087 even when ensuring the compiler isn't doing optimization tricks, using const-eval (which does correct IEEE 754 math) to multiply gives a different result than using the 8087 FPU multiply: https://rust.godbolt.org/z/KPE889hqq

    output:

    A: 0x3fffffffffffffff 1.9999999999999998
    B: 0x3ff0000000100000 1.0000000002328306
    EXPECTED: 0x40000000000fffff 2.000000000465661
    actual: 0x4000000000100000 2.0000000004656613
    

    this is because the 8087 multiply is done in f80 (which rounds because it's not big enough to represent the full product) then rounded to f64 which causes double-rounding errors.

    f32 doesn't have that double-rounding problem (when doing just one FP operation) because f80 is more than big enough to correctly represent the full product.

  17. LekKit commented on Jan 8, 2026

    @LekKit

    Also, LLVM will probably optimize away bitcasts when writing a series of FP operations.

    General idea: https://godbolt.org/z/Ec8MPoe4Y
    I am not sure how to reproduce this in idiomatic Rust, but the idea can be inferred by looking at generated LLVM IR. Note that only bitcasting between float->int needs to be "pessimized"

  18. LekKit commented on Jan 8, 2026

    @LekKit

    just using bitcasting from/to ints doesn't give correctly-rounded results for f64 with the 8087 even when ensuring the compiler isn't doing optimization tricks, using const-eval (which does correct IEEE 754 math) to multiply gives a different result than using the 8087 FPU multiply: https://rust.godbolt.org/z/KPE889hqq

    Good point. Isn't that solved by setting 8087 precision to 64 bits, or is that a different problem? Haven't looked into it in detail yet

  19. programmerjake commented on Jan 8, 2026

    @programmerjake
    Member

    Good point. Isn't that solved by setting 8087 precision to 64 bits, or is that a different problem? Haven't looked into it in detail yet

    it's only partially solved by setting the 8087 precision to 64 bits, f80 still has a much larger exponent range so also returns wrong results when the resulting f64 is subnormal even when precision is 64 bits. Java does some scaling before/after fp ops to make subnormal numbers work properly: #114479 (comment)

  20. johnwcowan commented on Apr 10, 2026

    @johnwcowan
  21. ssokolow commented on Apr 11, 2026

    @ssokolow
  22. RalfJung commented on Apr 11, 2026

    @RalfJung
    MemberAuthor
  23. DemiMarie commented on Apr 12, 2026

    @DemiMarie
    Contributor

    What about simply refusing to target such machines? In other words, always require SSE2 or softfloat, and in the latter case refuse to call any foreign function with a floating point argument not behind a pointer (because the ABI used would be wrong).

    Or just unconditionally fail the build.

  24. bjorn3 commented on Apr 12, 2026

    @bjorn3
    Member

    On 32bit x86 the ABI requires storing floats in x87 registers, so the only way to fully fix this issue is getting rid of 32bit x86 entirely, which is not an option at all for the forseeable future. We already avoid x87 registers in the Rust ABI irrespective of sse2, but can't fix the C ABI.

  25. RalfJung commented on Apr 12, 2026

    @RalfJung
    MemberAuthor

    the only way to fully fix this issue is getting rid of 32bit x86 entirely

    That's not true; as discussed above, one could generate code that stores/loads an f32/f64 in an x87 register while preserving the NaN payload. Someone "just" has to implement this in LLVM.

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-floating-pointArea: Floating point numbers and arithmeticC-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCI-miscompileIssue: Correct Rust code lowers to incorrect machine codeI-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessO-x86_32Target: x86 processors, 32 bit (like i686-*) (also known as IA-32, i386, i586, i686)P-mediumMedium priority

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions