Ensure floats are returned losslessly by the C ABI on 32-bit x86 - #161950
Ensure floats are returned losslessly by the C ABI on 32-bit x86#161950beetrees wants to merge 1 commit into
Conversation
|
|
This comment has been minimized.
This comment has been minimized.
|
@bors delegate try |
|
✌️ @beetrees, you can now perform try builds on this pull request! You can now post |
9749c87 to
477f3c8
Compare
|
@bors try |
This comment has been minimized.
This comment has been minimized.
Ensure floats are returned losslessly by the C ABI on 32-bit x86 try-job: i686-*
This comment has been minimized.
This comment has been minimized.
| C: HasDataLayout + HasTargetSpec, | ||
| { | ||
| if !fn_abi.ret.is_ignore() { | ||
| // "vectorcall" returns floats in `xmm0`, and soft float also does not use the x87 stack. |
There was a problem hiding this comment.
vectorcall does not show up in the code below, so this comment is a bit confusing.
There was a problem hiding this comment.
I've flipped the condition to make it more obvious what the comment is referring to.
There was a problem hiding this comment.
Shouldn't the FIXME and if be removed as this PR is supposed to fix exactly that?
There was a problem hiding this comment.
This FIXME isn't for the ABI issue, this is for the related but separate "i586 targets without SSE have horribly broken floats (#114479)". While this PR does fix the C ABI, it mostly doesn't have any noticeable affect on targets without SSE as LLVM will load f32/f64 into x87 registers (quietening NaNs) whenever it want to load them into a register, so f32/f64 are quietened mid function.
I've updated the test so it now runs on i586 targets while allowing NaNs to be quitened only on them to make it more obvious what's going on.
There was a problem hiding this comment.
Oh I see.
I was looking for a pure-Rust test for the change; it shouldn't be necessary to use C and FFI for that, should it?
There was a problem hiding this comment.
The "C" (and other ABI) test cases I added in this PR to return-float.rs are a pure-Rust test for this change: the version of return-float.rs in this PR would fail on i686-* targets before this PR (before this PR return-float.rs only tested the "Rust" ABI). After this PR, x87-related float miscompilations will be entirely limited to the non-SSE i586-* targets.
| /// 32-bit x86. This is needed as LLVM needs to generate extra code to ensure that signalling | ||
| /// NaNs are passed losslessly on the x87 floating point stack. Only valid on return types on | ||
| /// 32-bit x86 with a cast target of either `Reg::f32()` or `Reg::f64()`. | ||
| pub x87_floating_point_stack: bool, |
There was a problem hiding this comment.
This should mention that the LLVM-level type is then changed to x86_fp80.
| // any initialized bytes within a partially uninitialized value survive the round trip, | ||
| // freeze the value. | ||
| let value = if no_undef { value } else { self.freeze(value) }; | ||
| let is_nan = self.fcmp(RealPredicate::RealUNO, value, value); |
There was a problem hiding this comment.
Wouldn't the ideal test be "is it a signaling NaN"? Or do normal NaNs also get garbled?
Though I can imagine that that's annoying enough to implement that it's not worth it.
There was a problem hiding this comment.
Only signalling NaNs get quietened. I went with just checking for NaN as that is a single-instruction operation, whereas checking if a value is a signalling NaN requires many bit operations (more the larger the float). As the vast majority of values seem likely to be not NaNs at all, it felt more important to prioritise the fast path for non-NaNs.
There was a problem hiding this comment.
Makes sense. It's also unlikely LLVM would ever know "this is not a signaling NaN" (so the more complicated condition will also not have the branch eliminated more often).
Please add a comment explaining this.
68fdb80 to
bb9b569
Compare
| let (fraction_bits, fraction) = | ||
| if num_bits != 64 { (23, self.zext(bits, self.type_i64())) } else { (52, bits) }; |
There was a problem hiding this comment.
Is this clearer as
| let (fraction_bits, fraction) = | |
| if num_bits != 64 { (23, self.zext(bits, self.type_i64())) } else { (52, bits) }; | |
| let (fraction_bits, fraction) = match num_bits { | |
| 32 => (f32::MANTISSA_DIGITS - 1, self.zext(bits, self.type_i64())), | |
| 64 => (f64::MANTISSA_DIGITS - 1, bits), | |
| _ => bug!(), | |
| }; |
| let fraction = self.zext(fraction, self.type_ix(80)); | ||
|
|
||
| let is_nan_res = self.or(exp_and_sign, fraction); | ||
| let is_nan_res = self.bitcast(is_nan_res, self.type_x86_fp80()); |
There was a problem hiding this comment.
slightly confusing that this works, but apparently it does? (because when stored x86_fp80 would use 12 or 16 bytes).
There was a problem hiding this comment.
The LLVM x86_fp80 type is exactly 80 bits - padding is added when needed by Clang AFAICT:
| let fraction_bits = match num_bits { | ||
| 32 => 23, | ||
| 64 => 52, | ||
| _ => bug!("attempt to return float on x87 floating point stack with width {num_bits}"), | ||
| }; |
There was a problem hiding this comment.
again https://doc.rust-lang.org/std/primitive.f64.html#associatedconstant.MANTISSA_DIGITS etc. might be useful here.
| dst: PlaceRef<'tcx, Self::Value>, | ||
| ); | ||
| /// Losslessly convert a `f32` or `f64` to a float to be returned on the x87 floating point | ||
| /// stack. As `MaybeUninit` floats are returned on the floating point stack, `value` being |
There was a problem hiding this comment.
| /// stack. As `MaybeUninit` floats are returned on the floating point stack, `value` being | |
| /// stack. Because `MaybeUninit` floats are also returned on the floating point stack, `value` being |
Maybe this comment should also mention what this is for?
| @@ -44,6 +72,12 @@ where | |||
| // float aggregates directly in a floating-point register. | |||
| if fn_abi.ret.layout.is_single_fp_element(cx) { | |||
There was a problem hiding this comment.
hmm, is this actually what we want for is_single_fp_element? Should it not also return true on f16 and f128 (or have a clearer name)?
rust/compiler/rustc_abi/src/layout/ty.rs
Lines 158 to 176 in ee4a4a6
There was a problem hiding this comment.
Looking into it, it definitely isn't what we want, and at least on s390x #[repr(C)] struct Foo(f16) is passed incorrectly.
The rust version moves ldgr %f0, %r0 from a general into a float register. With the C compilers the value is already there.
I'll try to clean that up.
There was a problem hiding this comment.
I've left it as is in this PR as I didn't want to put to many changes in one PR, but is_single_fp_element is definitely not correct here. Both Clang and GCC also match f16 on x86, and they also require the struct to have no padding (which isn't checked here at the moment). is_single_fp_element also doesn't handle #[repr(transparent)] structs containing 1ZST fields. is_single_fp_element is also used in the s390x ABI, where GCC and Clang compilers currently implement slightly different rules: padding is fine as long as the overall size if either 2, 4 bytes or 8 bytes. This appears to be GCC and Clang compilers violating the s390x ABI specification, which says "A structure equivalent to one of the above. A structure is equivalent to a type 𝑇 if and only if it has exactly one member, which is either of type 𝑇 itself or a structure equivalent to type 𝑇.", which implies that padding should actually be ignored.
(EDIT: That's strange, GitHub didn't show me your second comment until after I'd posted mine.)
There was a problem hiding this comment.
Oh yeah is_single_fp_element is completely wrong.
Strange that tests/ui/abi/compatibility.rs didn't catch it, it's meant exactly for things like this. Even when I add an f32 test, which will check whether a repr(transparent) wrapper around f32 has the same ABI as f32, nothing fails: #161991
26bbbba to
552a4cb
Compare
This comment has been minimized.
This comment has been minimized.
552a4cb to
ec5132b
Compare
This comment has been minimized.
This comment has been minimized.
ec5132b to
0e8b455
Compare
View all comments
The x86 C ABI returns
f32andf64using the x87 extended precision floating point format (henceforthx86_fp80) on the x87 floating point stack. Currently, LLVM uses regularfld/fstpinstructions to convertf32/f64to and fromx86_fp80when returning from a function with the C ABI, however these instructions perform floating point format conversions that quieten signalling NaNs, which breaks Rust's (and LLVM's) guarantees. This is a long-standing LLVM bug (llvm/llvm-project#66803) but is difficult to fix on the LLVM side as it ties into larger problems with the way LLVM handles x87 registers holdingf32/f64, combined with the general lack of developer interest in 32-bit x86 without SSE (although the ABI bug specifically also affects targets with SSE2 enabled). This problem has been tracked on the Rust side in #115567, with #123351 ensuring that Rustic ABIs avoid the x87 stack altogether and #113053 adding notes to the platform support page detailing how the targets are non-complaint (including the tier 1 targetsi686-unknown-linux-gnuandi686-pc-windows-msvc).This PR fixes #115567 by manually converting NaNs to and from
x86_fp80when a function returnsf32/f64(this was briefly discussed in #t-compiler > `x87_f80` is weird @ 💬).r? @tgross35
cc @RalfJung
try-job: i686-*