Skip to content

fix overflow and precision loss in AccurateMath.IEEEremainder - #349

Open
insaf021 wants to merge 2 commits into
apache:masterfrom
insaf021:accuratemath-ieeeremainder-exact
Open

insaf021 wants to merge 2 commits into
apache:masterfrom
insaf021:accuratemath-ieeeremainder-exact

Conversation

@insaf021

@insaf021 insaf021 commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

With the default JdkMath backend (AccurateMath), on master:

IEEEremainder(1.7976931348623157E308, 1.5) = -Infinity               (StrictMath: 0.5)
IEEEremainder(4.503599627370496E15, 1.5)   = 1.0                     (StrictMath: -0.5)
IEEEremainder(1.0, 1.0E-300)               = 1.1102230246251565E-16  (StrictMath: -1.0368134719602886E-301)
IEEEremainder(1.5, 0.49999999999999994)    = 2.220446049250313E-16   (StrictMath: 1.6653345369377348E-16)

The general case evaluates dividend - divisor * rint(dividend / divisor) in doubles. Past 2^53 the rounded quotient is not the nearest integer, divisor * n is itself rounded so the error scales with the dividend rather than the remainder, and a large quotient overflows the product. Even with |x/y| <= 1000, 974017 of 1000000 random pairs differ from StrictMath. DerivativeStructure, SparseGradient and DSCompiler inherit this through JdkMath.

The fix reduces |dividend| modulo 2|divisor| with %, which is exact for doubles, then subtracts |divisor| at most twice to round the quotient to even, as fdlibm's __ieee754_remainder does. Every step is exact and the method stays pure Java. It matches StrictMath.IEEEremainder bit for bit on 100M pairs covering random bit patterns, subnormal divisors, ties and the overflow edges, on JDK 21 and 27. The javadoc bullet claiming the result always has the dividend's sign now applies to a zero result only, since IEEEremainder(3, 2) is -1.

testIEEEremainder compares against an exact BigDecimal reference; on master:

AccurateMathTest.testIEEEremainder:1376 expected: <0.5> but was: <-Infinity>

mvn clean verify is green across the reactor on JDK 21.

The general case computed dividend - divisor * rint(dividend / divisor) in
double arithmetic. Once the quotient passes 2^53 rint no longer sees the
nearest integer, divisor * n is itself rounded, and for large quotients the
product overflows, so IEEEremainder(Double.MAX_VALUE, 1.5) returned -Infinity.

Reduce |dividend| modulo 2 |divisor| with the exact % operator and subtract
|divisor| at most twice to round the quotient to even, as fdlibm does. The
result now matches StrictMath.IEEEremainder.
// twice to round the quotient to even, as in fdlibm __ieee754_remainder.
// All steps are exact: x + x only overflows when x + x > p holds anyway.
final double p = Math.abs(divisor);
double x = Math.abs(dividend) % (p + p);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can this use 2*p instead of p+p

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, it's the same value. Doubling a double is exact, and 2 * p overflows to infinity for exactly the same p as p + p, so the overflow path where % leaves x unchanged behaves as before. Switched in 319201c and AccurateMathTest still passes. The two x + x comparisons below are the same case if you'd prefer those as 2 * x as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please change all cases of (x + x) to 2 * x. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants