Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Two consecutive std::f32::sin() invocations on the same value give different results in certain scenarios #109118

Closed
aleksa2808 opened this issue Mar 14, 2023 · 8 comments · Fixed by #124609
Labels
A-docs Area: documentation for any part of the project, including the compiler, standard library, and tools A-floating-point Area: Floating point numbers and arithmetic A-LLVM Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues. C-bug Category: This is a bug. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@aleksa2808
Copy link

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:

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

@aleksa2808 aleksa2808 added the C-bug Category: This is a bug. label Mar 14, 2023
@aleksa2808
Copy link
Author

Looking at the assembly outputs I found out that the value 3f2fc6e9 is precalculated and present in the assembly, but the second value 3f2fc6ea is obtained from calling sinf at runtime, which I am not sure where it is located. So at optimization 0 no value is precalculated, in opt 2 and 3 both values are precalculated and in opt 1 only the first value is precalculated hence the mismatch. When the last println! is excluded both values are precalculated and present in the assembly even in opt level 1.

Here is the assembly I got from Compiler Explorer for opt-level=1:

example::main:
        push    r15
        push    r14
        push    r12
        push    rbx
        sub     rsp, 88
        mov     dword ptr [rsp + 12], 1061273700 ; value of a
        mov     dword ptr [rsp + 16], 1060095721 ; precalculated value of b
        lea     rax, [rsp + 16]
        mov     qword ptr [rsp + 24], rax
        mov     r12, qword ptr [rip + core::fmt::num::<impl core::fmt::LowerHex for u32>::fmt@GOTPCREL]
        mov     qword ptr [rsp + 32], r12
        lea     r15, [rip + .L__unnamed_1]
        mov     qword ptr [rsp + 56], r15
        mov     qword ptr [rsp + 64], 2
        mov     qword ptr [rsp + 40], 0
        lea     rbx, [rsp + 24]
        mov     qword ptr [rsp + 72], rbx
        mov     qword ptr [rsp + 80], 1
        mov     r14, qword ptr [rip + std::io::stdio::_print@GOTPCREL]
        lea     rdi, [rsp + 40]
        call    r14
        movss   xmm0, dword ptr [rsp + 12]
        call    qword ptr [rip + sinf@GOTPCREL] ; value of c calculated here
        movss   dword ptr [rsp + 20], xmm0
        lea     rax, [rsp + 20]
        mov     qword ptr [rsp + 24], rax
        mov     qword ptr [rsp + 32], r12
        mov     qword ptr [rsp + 56], r15
        mov     qword ptr [rsp + 64], 2
        mov     qword ptr [rsp + 40], 0
        mov     qword ptr [rsp + 72], rbx
        mov     qword ptr [rsp + 80], 1
        lea     rdi, [rsp + 40]
        call    r14
        lea     rax, [rsp + 12]
        mov     qword ptr [rsp + 24], rax
        mov     rax, qword ptr [rip + core::fmt::float::<impl core::fmt::Display for f32>::fmt@GOTPCREL]
        mov     qword ptr [rsp + 32], rax
        mov     qword ptr [rsp + 56], r15
        mov     qword ptr [rsp + 64], 2
        mov     qword ptr [rsp + 40], 0
        mov     qword ptr [rsp + 72], rbx
        mov     qword ptr [rsp + 80], 1
        lea     rdi, [rsp + 40]
        call    r14
        add     rsp, 88
        pop     rbx
        pop     r12
        pop     r14
        pop     r15
        ret

.L__unnamed_2:

.L__unnamed_3:
        .byte   10

.L__unnamed_1:
        .quad   .L__unnamed_2
        .zero   8
        .quad   .L__unnamed_3
        .asciz  "\001\000\000\000\000\000\000"

@aleksa2808
Copy link
Author

Looking at LLVM's constant folding code here it seems like it is using sin for both sin and sinf functions. Calling sin on 0.7568419 in C does give 3f2fc6e9 as the result, while calling sinf returns 3f2fc6ea. If this is true then this issue stems from LLVM and not from rustc.

@fmease

This comment was marked as resolved.

@rustbot rustbot added A-floating-point Area: Floating point numbers and arithmetic A-LLVM Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Mar 14, 2023
@scottmcm
Copy link
Member

scottmcm commented Mar 15, 2023

0.7568419_f32
= ​0.110000011100000001100100 (binary)
= 0.7568418979644775390625

sin(0.7568418979644775390625)
= 0.6866289073211929999283175613500157267691818642382142405237552360...
= 0.afc6e97d6ec7fa097fb482b992c988... (hex)
-> 0.101011111100011011101001 (binary, after truncation)
= 0.6866289_f32
= 0x3f2fc6e9 (as IEEE representation)

So the constant folded answer is the correct one, at least.

(It's off by -4.25×10-8 Np, whereas 0x3f2fc6ea is off by 4.43×10-8 Np.)

@aleksa2808
Copy link
Author

To summarize this issue:

Is there any guarantee in Rust that each call to math ops like f32::sin() on a certain number would provide the same result each time? Or can we only expect the result to be correct (as well as the same on repeated calculations) up only to a certain decimal?

@RalfJung
Copy link
Member

This is not a bug; optimizations leading to differences in the precision for these "imprecise" float operations is expected behavior. The underlying cause of this is that different libm implementations will provide different precision, so if the compiler pre-computes some results at compile time, that might yield different results than what the target would do at runtime.

So, this is at most a docs-issue. IMO it should be merged with #71355.

can we only expect the result to be correct (as well as the same on repeated calculations) up only to a certain decimal?

This is our current behavior and my understanding is that it is intended that way.

@RalfJung RalfJung added T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. A-docs Area: documentation for any part of the project, including the compiler, standard library, and tools and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jan 21, 2024
@Muon
Copy link

Muon commented May 2, 2024

The results of libm functions must not be constant-folded, unless we're OK with miscompilations a la #124364.

@RalfJung
Copy link
Member

RalfJung commented May 2, 2024

That's not necessarily true. As far as I am concerned, libm functions are non-deterministic. In that case it is okay to const-fold them even if their runtime behavior differs from the const-fold behavior. That is, for instance, how rust-lang/rfcs#3514 rationalizes const-fold producing different NaNs than what would happen at runtime.

#124364 is caused by the compiler assuming that float operations are deterministic. I don't think we have evidence that LLVM assumes libm functions to be deterministic, but I am not sure.

EDIT: Ah, never mind. #124364 is all about LLVM assuming that sin is deterministic. Fun, fun, fun.

fmease added a commit to fmease/rust that referenced this issue May 3, 2024
variable-precision float operations can differ depending on optimization levels

Follow-up to rust-lang#121793 and rust-lang#118217 that accounts for optimizations changing the precision of these functions.

Fixes rust-lang#109118
Fixes rust-lang#71355
fmease added a commit to fmease/rust that referenced this issue May 3, 2024
variable-precision float operations can differ depending on optimization levels

Follow-up to rust-lang#121793 and rust-lang#118217 that accounts for optimizations changing the precision of these functions.

Fixes rust-lang#109118
Fixes rust-lang#71355
fmease added a commit to fmease/rust that referenced this issue May 3, 2024
variable-precision float operations can differ depending on optimization levels

Follow-up to rust-lang#121793 and rust-lang#118217 that accounts for optimizations changing the precision of these functions.

Fixes rust-lang#109118
Fixes rust-lang#71355
@bors bors closed this as completed in c412751 May 3, 2024
rust-timer added a commit to rust-lang-ci/rust that referenced this issue May 3, 2024
Rollup merge of rust-lang#124609 - RalfJung:float-precision, r=cuviper

variable-precision float operations can differ depending on optimization levels

Follow-up to rust-lang#121793 and rust-lang#118217 that accounts for optimizations changing the precision of these functions.

Fixes rust-lang#109118
Fixes rust-lang#71355
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-docs Area: documentation for any part of the project, including the compiler, standard library, and tools A-floating-point Area: Floating point numbers and arithmetic A-LLVM Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues. C-bug Category: This is a bug. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants