Skip to content

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

Merged
aherbert merged 3 commits into
apache:masterfrom
insaf021:accuratemath-ieeeremainder-exact
Nov 13, 2026
Merged

aherbert merged 3 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.

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.

Done in 4ddfabc. Both comparisons and the comment above them now use 2 * x, and AccurateMathTest still passes.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.22%. Comparing base (f554608) to head (4ddfabc).
⚠️ Report is 416 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #349      +/-   ##
============================================
+ Coverage     86.54%   87.22%   +0.68%     
+ Complexity     9787       91    -9696     
============================================
  Files           532      499      -33     
  Lines         35516    33538    -1978     
  Branches       6194     5854     -340     
============================================
- Hits          30738    29255    -1483     
+ Misses         3518     3161     -357     
+ Partials       1260     1122     -138     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@aherbert
aherbert merged commit 9c50c75 into apache:master Nov 13, 2026
12 checks passed
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.

3 participants