Skip to content
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

[APFloat] Fix IEEEFloat::addOrSubtractSignificand and IEEEFloat::normalize #98721

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

beetrees
Copy link
Contributor

@beetrees beetrees commented Jul 13, 2024

Fixes #63895
Fixes #104984

Before this PR, addOrSubtractSignificand presumed that the loss came from the side being subtracted, and didn't handle the case where lhs == rhs and there was loss. This can occur during FMA. This PR fixes the situation by correctly determining where the loss came from and handling it appropriately.

Additionally, normalize failed to adjust the exponent when the significand is zero but lost_fraction != lfExactlyZero. This meant that the test case from #63895 was rounded incorrectly as the loss wasn't adjusted to account for the exponent being below the minimum exponent. This PR fixes this by only skipping the exponent adjustment if the significand is zero and there was no lost fraction.

(Note to reviewer: I don't have commit access)

@llvmbot
Copy link
Member

llvmbot commented Jul 13, 2024

@llvm/pr-subscribers-llvm-support

@llvm/pr-subscribers-llvm-adt

Author: None (beetrees)

Changes

Fixes #63895

Before this PR, addOrSubtractSignificand presumed that the loss came from the side being subtracted, and didn't handle the case where lhs == rhs and there was loss. This can occur during FMA. This PR fixes the situation by correctly determining where the loss came from and handling it appropriately.

Additionally, normalize failed to adjust the exponent when the significand is zero but lost_fraction != lfExactlyZero. This meant that the test case from #63895 was rounded incorrectly as the loss wasn't adjusted to account for the exponent being below the minimum exponent. This PR fixes this by only skipping the exponent adjustment if the significand is zero and there was no lost fraction.


Full diff: https://github.com/llvm/llvm-project/pull/98721.diff

2 Files Affected:

  • (modified) llvm/lib/Support/APFloat.cpp (+36-15)
  • (modified) llvm/unittests/ADT/APFloatTest.cpp (+10)
diff --git a/llvm/lib/Support/APFloat.cpp b/llvm/lib/Support/APFloat.cpp
index 3664de71d06df..46fe1af79a084 100644
--- a/llvm/lib/Support/APFloat.cpp
+++ b/llvm/lib/Support/APFloat.cpp
@@ -1607,7 +1607,8 @@ IEEEFloat::opStatus IEEEFloat::normalize(roundingMode rounding_mode,
   /* Before rounding normalize the exponent of fcNormal numbers.  */
   omsb = significandMSB() + 1;
 
-  if (omsb) {
+  // Only skip this `if` if the value is exactly zero.
+  if (omsb || lost_fraction != lfExactlyZero) {
     /* OMSB is numbered from 1.  We want to place it in the integer
        bit numbered PRECISION if possible, with a compensating change in
        the exponent.  */
@@ -1782,7 +1783,7 @@ IEEEFloat::opStatus IEEEFloat::addOrSubtractSpecials(const IEEEFloat &rhs,
 /* Add or subtract two normal numbers.  */
 lostFraction IEEEFloat::addOrSubtractSignificand(const IEEEFloat &rhs,
                                                  bool subtract) {
-  integerPart carry;
+  integerPart carry = 0;
   lostFraction lost_fraction;
   int bits;
 
@@ -1796,11 +1797,13 @@ lostFraction IEEEFloat::addOrSubtractSignificand(const IEEEFloat &rhs,
   /* Subtraction is more subtle than one might naively expect.  */
   if (subtract) {
     IEEEFloat temp_rhs(rhs);
+    auto lost_fraction_is_from_rhs = false;
 
     if (bits == 0)
       lost_fraction = lfExactlyZero;
     else if (bits > 0) {
       lost_fraction = temp_rhs.shiftSignificandRight(bits - 1);
+      lost_fraction_is_from_rhs = true;
       shiftSignificandLeft(1);
     } else {
       lost_fraction = shiftSignificandRight(-bits - 1);
@@ -1808,23 +1811,41 @@ lostFraction IEEEFloat::addOrSubtractSignificand(const IEEEFloat &rhs,
     }
 
     // Should we reverse the subtraction.
-    if (compareAbsoluteValue(temp_rhs) == cmpLessThan) {
-      carry = temp_rhs.subtractSignificand
-        (*this, lost_fraction != lfExactlyZero);
+    auto cmp_result = compareAbsoluteValue(temp_rhs);
+    if (cmp_result == cmpLessThan) {
+      auto borrow = false;
+      if (lost_fraction != lfExactlyZero && !lost_fraction_is_from_rhs) {
+        // The lost fraction is being subtracted, borrow from the significand
+        // and invert `lost_fraction`.
+        borrow = true;
+        if (lost_fraction == lfLessThanHalf)
+          lost_fraction = lfMoreThanHalf;
+        else if (lost_fraction == lfMoreThanHalf)
+          lost_fraction = lfLessThanHalf;
+      }
+      carry = temp_rhs.subtractSignificand(*this, borrow);
       copySignificand(temp_rhs);
       sign = !sign;
-    } else {
-      carry = subtractSignificand
-        (temp_rhs, lost_fraction != lfExactlyZero);
+    } else if (cmp_result == cmpGreaterThan) {
+      auto borrow = false;
+      if (lost_fraction != lfExactlyZero && lost_fraction_is_from_rhs) {
+        // The lost fraction is being subtracted, borrow from the significand
+        // and invert `lost_fraction`.
+        borrow = true;
+        if (lost_fraction == lfLessThanHalf)
+          lost_fraction = lfMoreThanHalf;
+        else if (lost_fraction == lfMoreThanHalf)
+          lost_fraction = lfLessThanHalf;
+      }
+      carry = subtractSignificand(temp_rhs, borrow);
+    } else { // cmpEqual
+      zeroSignificand();
+      if (lost_fraction != lfExactlyZero && lost_fraction_is_from_rhs) {
+        // rhs is slightly larger due to the lost fraction, flip the sign.
+        sign = !sign;
+      }
     }
 
-    /* Invert the lost fraction - it was on the RHS and
-       subtracted.  */
-    if (lost_fraction == lfLessThanHalf)
-      lost_fraction = lfMoreThanHalf;
-    else if (lost_fraction == lfMoreThanHalf)
-      lost_fraction = lfLessThanHalf;
-
     /* The code above is intended to ensure that no borrow is
        necessary.  */
     assert(!carry);
diff --git a/llvm/unittests/ADT/APFloatTest.cpp b/llvm/unittests/ADT/APFloatTest.cpp
index 86a25f4394e19..d6c488e75d335 100644
--- a/llvm/unittests/ADT/APFloatTest.cpp
+++ b/llvm/unittests/ADT/APFloatTest.cpp
@@ -560,6 +560,16 @@ TEST(APFloatTest, FMA) {
     EXPECT_EQ(-8.85242279E-41f, f1.convertToFloat());
   }
 
+  // Regression test for failing the `assert(!carry)` in
+  // `addOrSubtractSignificand`.
+  {
+    APFloat f1(-1.4728589E-38f);
+    APFloat f2(3.7105144E-6f);
+    APFloat f3(5.5E-44f);
+    f1.fusedMultiplyAdd(f2, f3, APFloat::rmNearestTiesToEven);
+    EXPECT_EQ(-0.0f, f1.convertToFloat());
+  }
+
   // Test using only a single instance of APFloat.
   {
     APFloat F(1.5);

@beetrees beetrees force-pushed the fix-apfloat-fma branch 2 times, most recently from cf8c805 to ecd06e2 Compare July 13, 2024 07:51
@tschuett tschuett requested a review from arsenm July 13, 2024 10:17
@tschuett
Copy link

The title claims to fix addOrSubtractSignificand and normalize, but there seems to be only a test for the former.

@tschuett tschuett added the floating-point Floating-point math label Jul 13, 2024
@beetrees
Copy link
Contributor Author

beetrees commented Jul 13, 2024

The test will fail if both fixes are not applied. I've updated the test comment to mention that.

@nikic nikic requested a review from efriedma-quic July 13, 2024 17:04
@tschuett
Copy link

ÌEEEFloat is currently final. Could make it not final with a comment: "not final for testing".
Make addOrSubtractSignificand protected. Then you can inherit from IEEEFloat for testing.

@beetrees
Copy link
Contributor Author

ÌEEEFloat is currently final. Could make it not final with a comment: "not final for testing". Make addOrSubtractSignificand protected. Then you can inherit from IEEEFloat for testing.

Done. I also had to make the fields of IEEEFloat protected.

@@ -306,7 +306,8 @@ struct APFloatBase {

namespace detail {

class IEEEFloat final : public APFloatBase {
// Not final for testing purposes only.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer to leave it as final

Choose a reason for hiding this comment

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

addOrSubtractSignificand went from private to protected. It is the only non-fishy way to get access for testing.

Comment on lines 7523 to 8199
// `addOrSubtractSignificand` only uses the sign, exponent and significand
this->sign = sign;
this->exponent = exponent;
this->significand.part = significand;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see why this justifies adding a subclass. You can just construct the appropriate value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The fields and addOrSubtractSignificand are protected so the only way to access them is via a subclass. Unfortunately just constructing a value via the public constructors is not sufficient as non-normalized significands are required to hit all the edge cases in addOrSubtractSignificand.

Copy link
Contributor

Choose a reason for hiding this comment

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

What's the problem with adding public accessors for these? Or adding a test class as a friend?

Copy link
Contributor

@arsenm arsenm left a comment

Choose a reason for hiding this comment

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

lgtm but I think there are better options than making this protected. Can the test function just be a friend, adding public accessors, or adding another base class for the tester subclass

Comment on lines 7523 to 8199
// `addOrSubtractSignificand` only uses the sign, exponent and significand
this->sign = sign;
this->exponent = exponent;
this->significand.part = significand;
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the problem with adding public accessors for these? Or adding a test class as a friend?

@beetrees
Copy link
Contributor Author

beetrees commented Aug 2, 2024

Either public accessors specifically for testing or a friend test class would also work. Is there any preference to which solution is best to use?

@RalfJung
Copy link
Contributor

@beetrees @arsenm what would it take to make progress on this PR? Would be nice to get this fixed :)

@beetrees
Copy link
Contributor Author

I've rebased to fix the merge conflict and added the test cases suggested in #104984. I've also changed the AddOrSubtractSignificand unit test to use a friend class as this can be done with only a single line needing to be added to APFloat.h.

@beetrees beetrees requested a review from tschuett November 25, 2024 18:57
@beetrees beetrees requested a review from arsenm November 25, 2024 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
5 participants