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

<{f16,f32,f64,f128} as Rem>::rem documented definition is misleading w.r.t. intermediate rounding #133758

Open
traviscross opened this issue Dec 2, 2024 · 12 comments
Labels
A-docs Area: Documentation for any part of the project, including the compiler, standard library, and tools C-bug Category: This is a bug. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Comments

@traviscross
Copy link
Contributor

traviscross commented Dec 2, 2024

Our documentation for impl Rem for {f16, f32, f64, f128} says:

The remainder has the same sign as the dividend and is computed as: x - (x / y).trunc() * y.

But that's not true. E.g.:

fn main() {
    let (x, y) = (11f64, 1.1f64);
    assert_eq!(x - (x / y).trunc() * y, x % y);
    //~^ PANIC
    // assertion `left == right` failed
    // left: 0.0
    // right: 1.0999999999999992
}

This mismatch creates a hazard when trying to correctly encode algorithms that rely on this semantic.

This tripped me up, e.g., when authoring:

cc #133485 #107904

cc @cuviper @Noratrieb @tczajka @Neutron3529 @BartMassey

@traviscross traviscross added the C-bug Category: This is a bug. label Dec 2, 2024
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Dec 2, 2024
@traviscross traviscross added T-libs Relevant to the library team, which will review and decide on the PR/issue. and removed needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. labels Dec 2, 2024
@traviscross
Copy link
Contributor Author

traviscross commented Dec 2, 2024

Notably, and perhaps uncoincidentally, C's fmod has the same documented behavior...

The floating-point remainder of the division operation x / y calculated by this function is exactly the value x - n * y, where n is x / y with its fractional part truncated.

...and the same mismatch:

unsafe extern "C" {
    safe fn fmod(x: f64, y: f64) -> f64;
}

fn main() {
    let (x, y) = (11f64, 1.1f64);
    assert_eq!(x - (x / y).trunc() * y, fmod(x, y));
    //~^ PANIC
    // assertion `left == right` failed
    // left: 0.0
    // right: 1.0999999999999992
}

@quaternic
Copy link

Indeed, Rust's floating point % is actually equivalent to fmod. The documentation is only correct if you interpret the expression x - (x / y).trunc() * y as an exact formula. (The wording of "computed as" certainly doesn't help one towards that reading.)

@traviscross
Copy link
Contributor Author

traviscross commented Dec 2, 2024

Yes, and the C docs state it even more clearly. They say, "it is exactly the value...".

@quaternic
Copy link

Yes, it is mathematically exact:

let x = 11_f64;  // == 11.0
let y = 1.1_f64; // == 1.100000000000000088817841970012523233890533447265625
                 // == 2476979795053773 / 2^51

// x / y == 11 / (2476979795053773 / 2^51)
//       == (11 * 2^51) / 2476979795053773
//       == 24769797950537728 / 2476979795053773
//       == 9 + 2476979795053771 / 2476979795053773 ( == 10 - 2 / 2476979795053773 )
// so the truncated quotient is 9
let iquot = 9;

// x - iquot * y == 11 - (9 * 2476979795053773 / 2^51)
//               == (11 * 2^51 - 9 * 2476979795053773) / 2^51
//               == 2476979795053771 / 2^51
//               == 1.099999999999999200639422269887290894985198974609375

assert_eq!(x % y, 1.099999999999999200639422269887290894985198974609375);

(sorry, the numbers get rather large with f64)

IMO, this is a bug in the documentation. Float % being equivalent to C's fmod is stated explicitly in https://rust-lang.github.io/rfcs/3514-float-semantics.html

@traviscross traviscross added the A-docs Area: Documentation for any part of the project, including the compiler, standard library, and tools label Dec 2, 2024
@traviscross
Copy link
Contributor Author

traviscross commented Dec 2, 2024

In my reading of the C23 (pre-)standard, section 7.12.10, either behavior would be acceptable:

The fmod functions return the value x − ny, for some integer n such that, if y is nonzero, the result has the same sign as x and magnitude less than the magnitude of y. If y is zero, whether a domain error occurs or the fmod functions return zero is implementation-defined.

That is, the standard is satisfied whether we choose 9 or 10 for n here.

@quaternic
Copy link

quaternic commented Dec 2, 2024

edit: I misinterpreted the C standard. See the next message.

Oh, I see. It's specified more loosely than the related remainder which is mandated by IEEE754.

So, fmod(11.0, 1.1) is allowed to return either of

  • 11.0 - 9.0 * 1.1 == 1.0999999999999996
  • 11.0 - 10.0 * 1.1 == 0.0

(in addition to the exact result of 1.0999999999999992_f64)

It's starting to look like a shaky foundation to be basing our operations on.

@tczajka
Copy link

tczajka commented Dec 3, 2024

That is, the standard is satisfied whether we choose 9 or 10 for n here.

So, fmod(11.0, 1.1) is allowed to return either of

  • 11.0 - 9.0 * 1.1 == 1.0999999999999996
  • 11.0 - 10.0 * 1.1 == 0.0

(in addition to the exact result of 1.0999999999999992_f64)

No, this is incorrect. The exact value is the only value that is allowed by the C standard (for an implementation that supports IEEE-754 with subnormals), as specified by F.10.7.1:

When subnormal results are supported, the returned value is exact and is independent of the current
rounding direction mode.

The C expression 11.0 - 10.0 * 1.1 is irrelevant. Section 7.12.10 says:

x − ny

That's a mathematical formula, not a C expression (as is clear by the lack of a * operator between n and y). For n=10 the value of the expression is negative which is not allowed.

@traviscross
Copy link
Contributor Author

traviscross commented Dec 4, 2024

Agreeing and extending, IEEE 754-2008 says in 5.3.1:

When y ≠ 0, the remainder r = remainder(x, y) is defined for finite x and y regardless of the rounding-direction attribute by the mathematical relation r = x − y × n , where n is the integer nearest the exact number x/y ; whenever | n − x/y | = ½ , then n is even. Thus, the remainder is always exact. If r = 0, its sign shall be that of x. remainder(x, ∞) is x for finite x.

Then C23 F.10.7.1 says:

The double version of fmod behaves as though implemented by:

#include <math.h>
#include <fenv.h>
#pragma STDC FENV_ACCESS ON
double fmod(double x, double y)
{
    double result;
    result = remainder(fabs(x), (y = fabs(y)));
    if (signbit(result)) result += y;
    return copysign(result, x);
}

So, for 11 and 1.1, remainder will choose n = 10, resulting in us calculating:

remainder(11., 1.1) = exact!(11. - 1.1 * 10.) = -10f64.mul_add(1.1, -11.) = -0.0000000000000008881784197001252;
fmod(11., 1.1) = remainder(11., 1.1) + 1.1 = 1.0999999999999992;

@11happy
Copy link

11happy commented Jan 20, 2025

Hello @traviscross , I am interested to work on this issue to fix div_euclid could you please assign this to me.
Thank you

@tczajka
Copy link

tczajka commented Jan 20, 2025

This is a duplicate of #107904.

@traviscross
Copy link
Contributor Author

Actually, I don't think this is a duplicate of that. This is a separate documentation bug. We just need to say explicitly in the docs that the stated calculation, x - (x / y).trunc() * y, is done in infinite precision rather than literally, which is how it currently reads and what the example given strongly suggests.

@tczajka
Copy link

tczajka commented Jan 21, 2025

OK but then title of the issue should be clarified. rem is in fact the (exact) remainder of truncated division, it should be documented as such. I agree the formula in code is misleading. Preferably it should be written as math, not as code.

@traviscross traviscross changed the title <{f16,f32,f64,f128} as Rem>::rem are not remainder of truncated division, as documented <{f16,f32,f64,f128} as Rem>::rem documented definition is misleading w.r.t. intermediate rounding Jan 21, 2025
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 C-bug Category: This is a bug. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

5 participants