Repository navigation
Tracking issue: 32bit x86 targets without SSE2 have unsound floating point behavior #114479
Description
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:
- Wrong optimization: instability of x87 floating-point results leads to nonsense llvm/llvm-project#44218: basically the same bug on the LLVM side, and pointing out the potential for soundness issues
- Tracking issue: 32bit x86 targets lose float NaN payload in return values #115567: that's a different problem, also affecting x86-32 but unrelated to what happens when an FP operation is executed; it is about the behavior of NaN bits as they get returned from a function
- Casts between floats and ints fail to roundtrip properly on
i586-unknown-linux-gnu#152635: also different problem, LLVM lowering of f->i casts on i586 produce incorrect results.
Prior issues:
Activity
DemiMarie commentedon Aug 4, 2023
Can we just drop support for x86 without SSE2 or fall back to software floating point?
RalfJung commentedon Aug 4, 2023
We currently have the following targets in that category:
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".
In theory of course we can, not sure if that would be any easier than following the Java approach.
programmerjake commentedon Aug 4, 2023
it should be much easier since LLVM already supports that, unlike the Java scheme (afaik)
https://gcc.godbolt.org/z/14WdnKhhP
RalfJung commentedon Aug 14, 2023
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...
[-]Tracking issue: 32bit x86 targets have non-compliant floating point behavior[/-][+]Tracking issue: 32bit x86 targets without SSE have non-compliant floating point behavior[/+][-]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[/+]RalfJung commentedon Sep 5, 2023
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?DemiMarie commentedon Sep 5, 2023
@RalfJung What about forcing non-SSE2 targets to software floating point? That must be supported anyway because of kernel code.
RalfJung commentedon Sep 5, 2023
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?
Rollup merge of rust-lang#113053 - RalfJung:x86_32-float, r=workingju…
85 remaining items
LekKit commentedon Jan 7, 2026
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/fstor integermovin 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.
RalfJung commentedon Jan 7, 2026
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.programmerjake commentedon Jan 7, 2026
just using bitcasting from/to ints doesn't give correctly-rounded results for
f64with 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/KPE889hqqoutput:
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 tof64which causes double-rounding errors.f32doesn't have that double-rounding problem (when doing just one FP operation) becausef80is more than big enough to correctly represent the full product.LekKit commentedon Jan 8, 2026
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"
LekKit commentedon Jan 8, 2026
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
programmerjake commentedon Jan 8, 2026
it's only partially solved by setting the 8087 precision to 64 bits,
f80still has a much larger exponent range so also returns wrong results when the resultingf64is subnormal even when precision is 64 bits. Java does some scaling before/after fp ops to make subnormal numbers work properly: #114479 (comment)johnwcowan commentedon Apr 10, 2026
ssokolow commentedon Apr 11, 2026
RalfJung commentedon Apr 11, 2026
DemiMarie commentedon Apr 12, 2026
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.
bjorn3 commentedon Apr 12, 2026
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.
RalfJung commentedon Apr 12, 2026
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.
lang/rust: enable SSE2 by default on i386
tests: fix mul_add_relaxed test on x87 targets; use assert_matches
lang/rust: enable SSE2 by default on i386