Skip to content

Fix fma for a tie with a far addend and for an infinite addend - #1523

Merged
mborland merged 2 commits into
boostorg:developfrom
ibmibmibm:fma-inf-addend
Oct 20, 2026
Merged

mborland merged 2 commits into
boostorg:developfrom
ibmibmibm:fma-inf-addend

Conversation

@ibmibmibm

Copy link
Copy Markdown
Contributor
  • fma for decimal32_t, decimal64_t and the fast types added the exact product and z with add_impl. If z was far below the product, the nearest modes dropped z, and a tie in the product went to the even neighbor.
  • fma now keeps the exact product of the significands in 64, 128 or 256 bits, and it does not move the product up. z moves to the product. A far z moves up p digits, and the product moves down to it.
  • The operand that moves down records a dropped digit that is not zero. After the sum, this record goes into the last digit. Because of this, a carry or a borrow does not remove it.
  • A sum of 2p - 1 or 2p digits rounds in one step with the finalizers of multiplication. Other sums round in the constructor. A zero operand leaves one operation, which rounds one time.
  • For a finite x and y and an infinite z, fma returned x * y + z. A large product rounded to an infinity, and with z of the other sign the sum was NaN. fma now returns z in this case, for all six types.
  • test_fma_tie tests ties with a far z, a dropped digit with a carry or a borrow, the bounds of the finalizers, and the five rounding modes. github_issue_1522 tests the infinite z.

Fixes #1522

Correctness
  • 3000000 random triples for each of decimal32_t, decimal64_t and decimal128_t, in the five rounding modes, against Intel libbid 2.0 Update 4. Each value that is not the same is an underflow error of libbid. Python decimal gives our value in each of these cases.
  • 4500000 triples with a far z, for all six types and the five modes, against Python decimal. All results are correct.
  • In the tie_far set of the benchmark, develop gives a different value from libbid for 6720 of 16384 decimal32_t triples and 6193 of 16384 decimal64_t triples. The fix gives 0.
  • test_fma, test_fma_tie, github_issue_1522 and test_cmath pass with GCC 16.2 and Clang 23.1 in C++14 and C++20 with -Wall -Wextra -Werror. They also pass without the builtin 128-bit integer, with BOOST_DECIMAL_FAST_MATH, and with BOOST_DECIMAL_NO_CONSTEVAL_DETECTION.
Speed

The table gives ns for each call, the best of 3 runs. Each set has 16384 triples: random significands and exponents (random), a tie with a far z (tie_far), a z near the product (near), a z near -(x * y) (cancel) and a zero z (zero_z). I used -O3 -march=native on an AMD Ryzen 9 3900X, on one core. libbid is built with GCC at -O2.

type set GCC develop GCC fix Clang develop Clang fix libbid
decimal32_t random 40.3 18.9 39.8 17.6 42.4
decimal32_t tie_far 28.0 15.6 30.6 15.0 42.3
decimal32_t near 45.6 22.7 42.8 22.0 37.9
decimal32_t cancel 27.5 15.3 28.3 14.4 16.8
decimal32_t zero_z 26.6 14.3 29.8 12.2 37.6
decimal64_t random 112.7 30.0 92.6 28.8 37.8
decimal64_t tie_far 55.4 26.1 49.4 23.1 29.4
decimal64_t near 159.9 30.3 124.6 30.2 46.0
decimal64_t cancel 62.3 17.1 60.0 16.1 27.0
decimal64_t zero_z 40.0 21.5 38.3 18.4 31.9
decimal128_t random 135.2 58.1 115.6 56.4 154.1
decimal128_t tie_far 118.6 55.0 99.9 49.3 129.3
decimal128_t near 131.6 59.2 111.9 59.1 177.1
decimal128_t cancel 44.9 41.7 46.4 49.1 91.6
decimal128_t zero_z 102.6 37.1 83.6 32.1 113.1
decimal_fast32_t random 37.1 13.5 36.6 15.9
decimal_fast32_t tie_far 26.1 10.1 26.8 13.4
decimal_fast32_t near 41.5 16.9 40.3 19.3
decimal_fast32_t cancel 23.6 13.4 25.1 13.0
decimal_fast32_t zero_z 24.9 6.8 25.8 9.1
decimal_fast64_t random 110.6 25.8 89.5 25.1
decimal_fast64_t tie_far 57.5 22.5 47.3 20.2
decimal_fast64_t near 153.5 26.3 120.7 26.8
decimal_fast64_t cancel 58.1 18.5 60.0 15.7
decimal_fast64_t zero_z 43.0 16.7 36.7 13.8
decimal_fast128_t random 135.3 58.4 118.8 56.5
decimal_fast128_t tie_far 121.5 55.0 102.4 50.0
decimal_fast128_t near 128.6 60.4 115.2 58.6
decimal_fast128_t cancel 49.1 46.9 46.9 51.6
decimal_fast128_t zero_z 102.5 35.8 86.1 33.5

With Clang, the 128-bit types are 3 to 5 ns slower in the cancel set.

Size

One function calls fma for each of the six types. The table gives the size of all .text sections of its object file, in bytes.

compiler flags develop fix
GCC 16.2 -O2 120683 117880
GCC 16.2 -Os 68440 68563
Clang 23.1 -O2 87270 82073
Clang 23.1 -Os 70472 68224

@codecov

codecov Bot commented Oct 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.4%. Comparing base (4a2cea3) to head (e55f42c).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##           develop   #1523     +/-   ##
=========================================
- Coverage     98.6%   98.4%   -0.2%     
=========================================
  Files          321     323      +2     
  Lines        27261   27444    +183     
  Branches      2285    2288      +3     
=========================================
+ Hits         26858   26981    +123     
- Misses         403     463     +60     
Files with missing lines Coverage Δ
include/boost/decimal/detail/cmath/fma.hpp 100.0% <100.0%> (ø)
test/github_issue_1522.cpp 100.0% <100.0%> (ø)
test/test_fma_tie.cpp 100.0% <100.0%> (ø)

... and 7 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4a2cea3...e55f42c. Read the comment docs.

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

@ibmibmibm
ibmibmibm marked this pull request as ready for review October 17, 2026 12:15
If x * y is a tie at the precision of the result, and z is far below
it, the sign of z decides the tie in the nearest modes. fma for
decimal32_t and decimal64_t added the operands with add_impl, which
dropped z when the operands have more digits than the result.

fma now keeps the exact product of the significands in 64, 128 or 256
bits, and it does not move the product up. z moves to the product, but
a far z moves up p digits and the product moves down to it. The
operand that moves down records a dropped digit that is not zero.
After the sum, that record goes into the last digit, thus a carry or a
borrow does not remove it. A sum of 2p - 1 or 2p digits rounds in one
step with the finalizers of multiplication, and other sums round in
the constructor. A zero operand leaves one operation, which rounds one
time.

Fixes boostorg#1522
When an operand of fma was not finite, fma returned x * y + z. If x
and y are finite but their product is above the largest value, x * y
rounds to an infinity, and with z an infinity of the other sign the
sum is NaN. The exact product is finite, so the correct result is z:
fma(max, 10, -inf) must be -inf. fma now returns z in this case for
all six types, and keeps x * y + z for the other cases.

Fixes boostorg#1522
@mborland

Copy link
Copy Markdown
Member

There was a merge conflict from your other PRs so the only change I made was to rebase this PR on develop

@mborland
mborland merged commit ac91059 into boostorg:develop Oct 20, 2026
69 of 73 checks passed
@ibmibmibm
ibmibmibm deleted the fma-inf-addend branch October 20, 2026 17:04
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.

fma drops a small addend in a tie, and gives NaN for a large finite product and an infinite addend

2 participants