Skip to content

Conversation

Jules-Bertholet
Copy link
Contributor

(float_ty::MAX / 2) - (float_ty::MIN_POSITIVE * 2) equals (float_ty::MAX / 2) + (float_ty::MIN_POSITIVE * 2) equals (float_ty::MAX / 2). So these branches are pointless.

CC @Urgau who wrote the original implementation in #92048; does this seem right?

@rustbot label A-floating-point

`(float_ty::MAX / 2) - (float_ty::MIN_POSITIVE * 2)` equals
`(float_ty::MAX / 2) + (float_ty::MIN_POSITIVE * 2)` equals
`(float_ty::MAX / 2)`. So these branches are pointless
@rustbot
Copy link
Collaborator

rustbot commented Sep 5, 2025

r? @jhpratt

rustbot has assigned @jhpratt.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. A-floating-point Area: Floating point numbers and arithmetic labels Sep 5, 2025
Copy link
Contributor

@tgross35 tgross35 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense to me but I'll wait for Urgau to double check. I assume the original libcxx version may have been accounting for ftz or something.

One extra nice thing about f16 is it's feasible to test 2 to 3-dimensional operations exhaustively (passed for me):

#![feature(f16)]

pub const fn midpoint1(a: f16, b: f16) -> f16 {
    const LO: f16 = f16::MIN_POSITIVE * 2.;
    const HI: f16 = f16::MAX / 2.;
    let abs_a = a.abs();
    let abs_b = b.abs();
    if abs_a <= HI && abs_b <= HI {
        // Overflow is impossible
        (a + b) / 2.
    } else if abs_a < LO {
        // Not safe to halve `a` (would underflow)
        a + (b / 2.)
    } else if abs_b < LO {
        // Not safe to halve `b` (would underflow)
        (a / 2.) + b
    } else {
        // Safe to halve `a` and `b`
        (a / 2.) + (b / 2.)
    }
}

pub const fn midpoint2(a: f16, b: f16) -> f16 {
    const HI: f16 = f16::MAX / 2.;
    let abs_a = a.abs();
    let abs_b = b.abs();
    if abs_a <= HI && abs_b <= HI {
        // Overflow is impossible
        (a + b) / 2.
    } else {
        // Safe to halve `a` and `b`
        (a / 2.) + (b / 2.)
    }
}

fn main() {
    for i in 0..=u16::MAX {
        for j in 0..=u16::MAX {
            let a = f16::from_bits(i);
            let b = f16::from_bits(j);
            let m1 = midpoint1(a, b);
            let m2 = midpoint2(a, b);
            assert_eq!(m1.to_bits(), m2.to_bits());
        }
    }
}

View changes since this review

@tgross35 tgross35 assigned tgross35 and unassigned jhpratt Sep 5, 2025
@tgross35 tgross35 requested a review from Urgau September 5, 2025 06:33
@Jules-Bertholet Jules-Bertholet changed the title Simplify {f16, f32, f54, f128}::midpoint() Simplify {f16, f32, f64, f128}::midpoint() Sep 5, 2025
@GrigorenkoPV
Copy link
Contributor

GrigorenkoPV commented Sep 5, 2025

Tested f16 (took 2s), f32 (took 13s), and f64 (took 101s) with kani.

#![feature(f128, f16)]

type F = f64;

pub fn old(a: F, b: F) -> F {
    const LO: F = F::MIN_POSITIVE * 2.;
    const HI: F = F::MAX / 2.;

    let abs_a = a.abs();
    let abs_b = b.abs();

    if abs_a <= HI && abs_b <= HI {
        // Overflow is impossible
        (a + b) / 2.
    } else if abs_a < LO {
        // Not safe to halve `a` (would underflow)
        a + (b / 2.)
    } else if abs_b < LO {
        // Not safe to halve `b` (would underflow)
        (a / 2.) + b
    } else {
        // Safe to halve `a` and `b`
        (a / 2.) + (b / 2.)
    }
}

pub fn new(a: F, b: F) -> F {
    const HI: F = F::MAX / 2.;

    let abs_a = a.abs();
    let abs_b = b.abs();

    if abs_a <= HI && abs_b <= HI {
        // Overflow is impossible
        (a + b) / 2.
    } else {
        (a / 2.) + (b / 2.)
    }
}

#[cfg(kani)]
#[kani::proof]
fn equiv() {
    let a: F = kani::any();
    let b: F = kani::any();
    kani::assume(!(a == -b && a.abs() == F::INFINITY));
    let prev = old(a, b);
    let next = new(a, b);
    assert_eq!(prev.is_nan(), next.is_nan());
    if !prev.is_nan() && !next.is_nan() {
        assert_eq!(prev, next)
    }
}

It's telling me the implementations are equivalent. The only thing it complained about was "addition with NaN" (though I believe that is well-defined and legal) which occurs when the arguments are the opposite infinities. Though, both implementations return NaN in that case, from what I can tell.

Currently testing f128. I guess it is going to take around 1000s, so see y'all in 15min or so.

One thing I've noticed during the testing is that f128 seems to lack a Display impl, but it has a Debug impl. Is that intentional?


UPD: f128 succeeded too, in mere 807s.

@Jules-Bertholet
Copy link
Contributor Author

Jules-Bertholet commented Sep 5, 2025

One thing I've noticed during the testing is that f128 seems to lack a Display impl, but it has a Debug impl. Is that intentional?

It’s a known TODO. #116909 (“Figure out f128 which will not be straightforward.”)

Copy link
Member

@Urgau Urgau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me as well.

However, I also wonder why libcxx implementation was and still does that. Perhaps to save a division for performance reasons?

View changes since this review

@Jules-Bertholet
Copy link
Contributor Author

Jules-Bertholet commented Sep 5, 2025

Perhaps to save a division for performance reasons?

Perhaps. But in that case, they/we could skip the addition as well.

@quaternic
Copy link
Contributor

I assume the original libcxx version may have been accounting for ftz or something.

I think it's pretty much this, or more generally supporting non-default floating point environments. The condition x.abs() < LO is exactly when x / 2.0 is either zero or subnormal. The original implementation avoids causing a spurious FE_UNDERFLOW in the latter case. In those cases the addition like a + (b / 2.0) is then used to nudge the result in the correct direction according to the current rounding mode and set FE_INEXACT as appropriate.

Rust doesn't support any of that so the patch seems fine to me.

@tgross35
Copy link
Contributor

tgross35 commented Sep 5, 2025

One thing I've noticed during the testing is that f128 seems to lack a Display impl, but it has a Debug impl. Is that intentional?

It’s a known TODO. #116909 (“Figure out f128 which will not be straightforward.”)

For a bit more on this, we can't use the generic implementation used for other types because it uses u64s and we'd pay a cost there. It's on my todo list but a much bigger change than f16 Display, which was a pretty big change :)

@tgross35
Copy link
Contributor

tgross35 commented Sep 5, 2025

@bors r+

@bors
Copy link
Collaborator

bors commented Sep 5, 2025

📌 Commit bc17bcd has been approved by tgross35

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 5, 2025
bors added a commit that referenced this pull request Sep 5, 2025
Rollup of 11 pull requests

Successful merges:

 - #138944 (Add `__isPlatformVersionAtLeast` and `__isOSVersionAtLeast` symbols)
 - #139113 (unstable book: in a sanitizer example, check the code)
 - #145735 (style-guide: Document absence of trailing whitespace)
 - #146041 (tidy: --bless now makes escheck run with --fix)
 - #146144 (compiler: Apply target features to the entry function)
 - #146225 (Simplify `{f16, f32, f64, f128}::midpoint()`)
 - #146234 (change file-is-generated doc comment to inner)
 - #146241 (rustc_infer: change top-level doc comment to inner)
 - #146242 (Ensure that `--html-after-content` option is used to check `scrape_examples_ice` rustdoc GUI test)
 - #146243 (remove couple of redundant clones)
 - #146250 (Bump stage0 rustfmt)

Failed merges:

 - #146200 (Simplify rustdoc-gui tester by calling directly browser-ui-test)

r? `@ghost`
`@rustbot` modify labels: rollup
@bors bors merged commit 690753f into rust-lang:master Sep 5, 2025
10 checks passed
@rustbot rustbot added this to the 1.91.0 milestone Sep 5, 2025
rust-timer added a commit that referenced this pull request Sep 5, 2025
Rollup merge of #146225 - Jules-Bertholet:simplify-float-midpoint, r=tgross35

Simplify `{f16, f32, f64, f128}::midpoint()`

`(float_ty::MAX / 2) - (float_ty::MIN_POSITIVE * 2)` equals `(float_ty::MAX / 2) + (float_ty::MIN_POSITIVE * 2)` equals `(float_ty::MAX / 2)`. So these branches are pointless.

CC `@Urgau` who wrote the original implementation in #92048; does this seem right?

`@rustbot` label A-floating-point
@Jules-Bertholet Jules-Bertholet deleted the simplify-float-midpoint branch September 6, 2025 00:30
github-actions bot pushed a commit to model-checking/verify-rust-std that referenced this pull request Sep 9, 2025
…point, r=tgross35

Simplify `{f16, f32, f64, f128}::midpoint()`

`(float_ty::MAX / 2) - (float_ty::MIN_POSITIVE * 2)` equals `(float_ty::MAX / 2) + (float_ty::MIN_POSITIVE * 2)` equals `(float_ty::MAX / 2)`. So these branches are pointless.

CC `@Urgau` who wrote the original implementation in rust-lang#92048; does this seem right?

`@rustbot` label A-floating-point
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-floating-point Area: Floating point numbers and arithmetic S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants