When using float comparison in const fn
, e.g.
const fn eq(a: f64, b: f64) -> bool {
return a == b
}
Playground: Rust Playground
The compiler refuses with the error:
error[E0658]: floating point arithmetic is not allowed in constant functions
But it accepts the same code in a const
context outside a function:
fn main() {
const A: f64 = 1.0;
const B: f64 = 1.0;
const RES: bool = A == B;
println!("{RES}");
}
Playground: Rust Playground
But is that code sound, with what that issue said? Tracking issue for `#![feature(const_fn_floating_point_arithmetic)]` · Issue #57241 · rust-lang/rust · GitHub
1 Like
As mentioned in the issue you linked, the problem is that a const fn
can be evaluated both at compile-time and at run-time, and the results might differ.
const RES: bool = A == B;
does not have this problem because it is only evaluated at compile time.
9 Likes
But it also means that if cross compiled, the result may be different from the result if compiled on the target arch (for the same reason that floating point arithmetic is not allowed in constant functions), right?
No. Compile-time evaluation uses a software implementation of floating-point arithmetic, so the results do not depend on the host hardware or the target hardware. The value of the const will be the same regardless of what machine the program is (cross-)compiled on or for.
3 Likes
jjpe
May 10, 2024, 3:51am
5
NOT A CONTRIBUTION
In the context of const fns being able to return different results when evaluated at compile time vs runtime, is this a reason why?
I can imagine that while a software implementation would return the same results on every arch, different arches can give different results within some reasonably strict bounds¹.
¹ Those bounds are mainly that I'd expect any modern FP hardware implementations to adhere to some reasonably recent version of IEEE-754, rather than just providing whatever FP instructions it feels like.
1 Like
CAD97
May 10, 2024, 4:08am
6
The primary notable case are NaN values, which are only loosely specified by the IEEE standard. More specifically, what the NaN payload bits are. As you note, non-NaN values should always be bit-identical, at least for the primitive arithmetic operations which are correctly rounded ±½ ULP.
2 Likes
Floating point is not even sound in non-const contexts, but these are bugs in the compiler.
opened 01:04PM - 14 Mar 23 UTC
closed 01:20PM - 03 May 24 UTC
A-LLVM
T-libs-api
A-docs
C-bug
A-floating-point
I noticed that, in certain scenarios, two consecutive `std::f32::sin()` function… invocations can give two different results.
A scenario where I observed this is the following:
```rust
fn main() {
let a: f32 = 0.7568419;
assert_eq!(
format!("{:x}", a.sin().to_bits()),
format!("{:x}", a.sin().to_bits())
);
println!("{a}");
}
```
When the `opt-level` is set to 1, the assert fails with the following output:
```
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `"3f2fc6e9"`,
right: `"3f2fc6ea"`', src\main.rs:4:5
```
When the `opt-level` is 0, both outputs are `3f2fc6ea`, and when it is 2 or 3 both outputs are `3f2fc6e9`.
Another surprising thing for me is that when I remove the last println (`println!("{a}");`), the assert passes even for `opt-level=1`. In that case, values are `3f2fc6ea` only when the `opt-level` is 0, while for other values they are `3f2fc6e9`.
### Meta
`rustc --version --verbose`:
```
rustc 1.68.0 (2c8cc3432 2023-03-06)
binary: rustc
commit-hash: 2c8cc343237b8f7d5a3c3703e3a87f2eb2c54a74
commit-date: 2023-03-06
host: x86_64-pc-windows-msvc
release: 1.68.0
LLVM version: 15.0.6
```
This also occurs on the following nightly version:
```
rustc 1.70.0-nightly (7b4f48927 2023-03-12)
binary: rustc
commit-hash: 7b4f48927dce585f747a58083b45ab62b9d73a53
commit-date: 2023-03-12
host: x86_64-pc-windows-msvc
release: 1.70.0-nightly
LLVM version: 15.0.7
```
</p>
</details>
opened 07:46AM - 24 Apr 24 UTC
miscompilation
llvm:SCEV
The following program (based on the Rust version from [here](https://github.com/… rust-lang/rust/issues/114479#issuecomment-2072945866), which is based on @comex's example from a different issue; they gave an explanation of how they found it [here](https://github.com/rust-lang/unsafe-code-guidelines/issues/471#issuecomment-1774261953))
```c
#include <stdio.h>
#include <stddef.h>
void print_vals(float x, size_t i, int vals_i) {
printf("x=%f i=%zu vals[i]=%i\n", x, i, vals_i);
}
void out_of_bounds(float x, size_t i) {
printf("x=%f i=%zu vals[i]=out of bounds\n", x, i);
}
void evil(int vals[300]) {
float x = 0.0;
size_t i = 0;
while (x != 90.0) {
// At compile time, LLVM will brute-force the loop and discover that it
// terminates after 90 iterations, with `i` always less than 300. This bounds
// check therefore gets optimised out.
if (i < 300) {
print_vals(x, i, vals[i]);
} else {
out_of_bounds(x, i);
}
x += 1.0;
// This addition is too small to have any effect on the value of `x` with
// regular `float` precision, which is what LLVM uses at compile-time.
// However, with the extra precision offered by x87 floating point registers,
// the value of `x` will change slightly, meaning it will never hit exactly
// 90.0 and therefore the loop will never terminate at runtime.
x += 2.9802322387695312e-08;
i += 2;
}
}
int main() {
int vals[300];
for (int i = 0; i < 300; i++) {
vals[i] = i;
}
evil(vals);
}
```
compiled with `clang -O3 --target=i686-unknown-linux-gnu -mno-sse code.c` will segfault at runtime. This is due to LLVM evaluating floats at standard float precision at compile-time, but outputting machine code that uses x87 extended precision at runtime. Specifically, [llvm/lib/Analysis/ScalarEvolution.cpp](https://github.com/llvm/llvm-project/blob/46b011d0ccb468613bcc7e9e756518f9f383001d/llvm/lib/Analysis/ScalarEvolution.cpp) will brute force the loop using compile-time semantics, causing the bounds check to be optimised out; however the extra precision of x87 extended precision floats will mean that the loop termination condition is never hit at runtime.
The [LangRef](https://llvm.org/docs/LangRef.html#floatenv) appears to imply that the compile-time semantics are correct, so this is a bug in the x86 backend.
Related to #44218.
opened 05:58AM - 25 Apr 24 UTC
A-LLVM
A-cross
P-high
I-unsound
C-bug
A-floating-point
I tried this code (based on an example from [another issue](https://github.com/r… ust-lang/rust/issues/114479#issuecomment-2072945866), which is lightly adapted from @comex's example on a [different issue](https://github.com/rust-lang/unsafe-code-guidelines/issues/471#issuecomment-1774261953)):
```rust
#[inline(never)]
fn print_vals(x: f64, i: usize, vals_i: u32) {
println!("x={x} i={i} vals[i]={vals_i}");
}
#[inline(never)]
pub fn evil(vals: &[u32; 300]) {
// Loop variables:
let mut x: f64 = 1.0; // x = x.sin() every time
let mut i: usize = 0; // increments by 2 every time
while x != 0.17755388399451055 {
// LLVM will do a brute-force evaluation of this loop for up to 100
// iterations to try to calculate an iteration count. (See
// `llvm/lib/Analysis/ScalarEvolution.cpp`.) Under host floating
// point semantics (on x86_64-unknown-linux-gnu), `x` will equal exactly
// 0.17755388399451055 after 90 iterations; LLVM discovers this by
// brute-force evaluation and concludes that the iteration count is
// always 90.
// Now, if this loop executes 90 times, then `i` must be in the range
// `0..180`, so the bounds check in `vals[i]` should always pass, so
// LLVM eliminates it.
print_vals(x, i, vals[i]);
// Update `x`. The exact computation doesn't matter that much; it just
// needs to:
// (a) be possible to constant-evaluate by brute force (i.e. by going
// through each iteration one at a time);
// (b) be too complex for IndVarSimplifyPass to simplify *without*
// brute force;
// (b) differ depending on the current platforms floating-point math
// implementation.
// `sin` is one such function.
x = x.sin();
// Update `i`, the integer we use to index into `vals`. Why increment
// by 2 instead of 1? Because if we increment by 1, then LLVM notices
// that `i` happens to be equal to the loop count, and therefore it can
// replace the loop condition with `while i != 90`. With `i` as-is,
// LLVM could hypothetically replace the loop condition with
// `while i != 180`, but it doesn't.
i += 2;
}
}
pub fn main() {
// Make an array on the stack:
let mut vals: [u32; 300] = [0; 300];
for i in 0..300 { vals[i as usize] = i; }
evil(&vals);
}
```
I expected to see this happen: The 100% safe code never segfaults when compiled with optimisations.
Instead, this happened: When cross-compiled with optimisations to a platform with a `sin` implementation that does not produce identical results to the platform on which the code is being was compiled, the resulting binary will segfault. I discovered this by cross-compiling from x86_64-unknown-linux-gnu to x86_64-pc-windows-msvc and running the resulting binary with wine, but any pair of platforms with differing `sin` implementations will do.
### Meta
`rustc --version --verbose`:
```
rustc 1.77.2 (25ef9e3d8 2024-04-09)
binary: rustc
commit-hash: 25ef9e3d85d934b27d9dada2f9dd52b1dc63bb04
commit-date: 2024-04-09
host: x86_64-unknown-linux-gnu
release: 1.77.2
LLVM version: 17.0.6
```
1 Like
system
Closed
August 9, 2024, 2:55pm
8
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.