8000 Further optimize Math.rint using a floating point splitting technique. by sjrd · Pull Request #5200 · scala-js/scala-js · GitHub
[go: up one dir, main page]

Skip to content

Further optimize Math.rint using a floating point splitting technique. #5200

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

Merged
merged 1 commit into from
Jun 21, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 29 additions & 62 deletions javalib/src/main/scala/java/lang/Math.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,71 +53,38 @@ object Math {

// Wasm intrinsic
def rint(a: scala.Double): scala.Double = {
/* Is the integer-valued `x` odd? Fused by hand of `(x.toLong & 1L) != 0L`.
* Corner cases: returns false for Infinities and NaN.
*/
@inline def isOdd(x: scala.Double): scala.Boolean =
(x.asInstanceOf[js.Dynamic] & 1.asInstanceOf[js.Dynamic]).asInstanceOf[Int] != 0

/* js.Math.round(a) does *almost* what we want. It rounds to nearest,
* breaking ties *up*. We need to break ties to *even*. So we need to
* detect ties, and for them, detect if we rounded to odd instead of even.
*
* The reasons why the apparently simple algorithm below works are subtle,
* and vary a lot depending on the range of `a`:
*
* - a is NaN
* r is NaN, then the == is false
* -> return r
*
* - a is +-Infinity
* r == a, then == is true! but isOdd(r) is false
* -> return r
*
* - 2**53 <= abs(a) < Infinity
* r == a, r - 0.5 rounds back to a so == is true!
* fortunately, isOdd(r) is false because all a >= 2**53 are even
* -> return r
/* We apply the technique described in Section II of
* Claude-Pierre Jeannerod, Jean-Michel Muller, Paul Zimmermann.
* On various ways to split a floating-point number.
* ARITH 2018 - 25th IEEE Symposium on Computer Arithmetic,
* Jun 2018, Amherst (MA), United States.
* pp.53-60, 10.1109/ARITH.2018.8464793. hal-01774587v2
* available at
* https://hal.inria.fr/hal-01774587v2/document
* with β = 2, p = 53, and C = 2^(p-1) = 2^52.
*
* - 2**52 <= abs(a) < 2**53
* r == a (because all a's are integers in that range)
* - a is odd
* r - 0.5 rounds down (towards even) to r - 1.0
* so a == r - 0.5 is false
* -> return r
* - a is even
* r - 0.5 rounds back up! (towards even) to r
* so a == r - 0.5 is true!
* but, isOdd(r) is false
* -> return r
* That is only valid for values x <= 2^52. Fortunately, all values that
* are >= 2^52 are already integers, so we can return them as is.
*
* - 0.5 < abs(a) < 2**52
* then -2**52 + 0.5 <= a <= 2**52 - 0.5 (because values in-between are not representable)
* since Math.round rounds *up* on ties, r is an integer in the range (-2**52, 2**52]
* r - 0.5 is therefore lossless
* so a == r - 0.5 accurately detects ties, and isOdd(r) breaks ties
* -> return `r`` or `r - 1.0`
*
* - a == +0.5
* r == 1.0
* a == r - 0.5 is true and isOdd(r) is true
* -> return `r - 1.0`, which is +0.0
*
* - a == -0.5
* r == -0.0
* a == r - 0.5 is true and isOdd(r) is false
* -> return `r`, which is -0.0
*
* - 0.0 <= abs(a) < 0.5
* r == 0.0 with the same sign as a
* a == r - 0.5 is false
* -> return r
* We cannot use "the 1.5 trick" with C = 2^(p-1) + 2^(p-2) to handle
* negative numbers, because that would further reduce the range of valid
* `x` to maximum 2^51, although we actually need it up to 2^52. Therefore,
* we have a separate branch for negative numbers. This also allows to
* gracefully deal with the fact that we need to return -0.0 for values in
* the range [-0.5,-0.0).
*/
val r = js.Math.round(a)
if ((a == r - 0.5) && isOdd(r))
r - 1.0
else
r
val C = 4503599627370496.0 // 2^52
if (a > 0) {
if (a >= C) a
else (C + a) - C
} else if (a < 0) {
// do not "optimize" as `C - (C - a)`, as it would return +0.0 where it should return -0.0
if (a <= -C) a
else -((C - a) - C)
} else {
// Handling zeroes here avoids the need to distinguish +0.0 from -0.0
a // 0.0, -0.0 and NaN
}
}

@inline def round(a: scala.Float): scala.Int = js.Math.round(a).toInt
Expand Down
0