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

AMM Add Number class and associated algorithms #4145

Closed
wants to merge 10 commits into from

Conversation

HowardHinnant
Copy link
Contributor

High Level Overview of Change

Initial check-in for general purpose decimal floating point Number type with power raised to rational exponent algorithm.

Context of Change

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking change that only restructures code)
  • Tests (You added tests for code that already exists, or your new feature included in this PR)
  • Documentation Updates
  • Release

* Conversions to Number are implicit
* Conversions away from Number are explicit and potentially lossy
* If lossy, round to nearest, and to even on tie
Copy link
Collaborator

@gregtatcam gregtatcam left a comment

Choose a reason for hiding this comment

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

LGTM. I reviewed/tested only from integration into AMM point of view.

Copy link
Collaborator

@scottschurr scottschurr left a comment

Choose a reason for hiding this comment

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

I'm not a good enough numerics person to have an opinion on the algorithms here. So I have to rely on tests returning results that I'd expect. There are some places where the test coverage is not very good, so I'd very much like to see some additional coverage. That will give me better confidence with the algorithms. Finding the cube root of -27 might be interesting, for example. And maybe exercising some of the places that you expect errors to be thrown.

Beyond that I had a few constexpr, naming, and structural thoughts for you to consider. But they are at your discretion. Let me know what you think. Thanks.

{
using rep = std::int64_t;
rep mantissa_{0};
int exponent_{-2'147'483'648};
Copy link
Collaborator

Choose a reason for hiding this comment

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

I might be inclined to use std::int32_t rather than int. That will increase behavioral similarities across platforms.

I'd also be inclined to use std::numeric_limits<std::int32_t>::lowest() in place of -2'147'483'648.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I attempted to minimize differences between Number and IOUAmount. The latter uses int here. However I've adopted your suggestion about numeric_limits.

explicit unchecked() = default;
};

explicit Number() = default;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the compiler treats this constructor as constexpr since both members are initialized, so it might be nice to mark it constexpr explicitly.

Comment on lines +50 to +51
Number(rep mantissa);
explicit Number(rep mantissa, int exponent);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think these two constructors could be declared constexpr if we made normalize() constexpr and moved the normalize() implementation into the header file. I don't see anything in the normalize() implementation that would prevent if from being used during constant evaluation. But maybe I'm missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd rather not move normalize() into the header because of its size. The compiler probably won't inline it, which is good, but then the linker has to spend time eliminating duplicate outlined copies, which is bad. My policy is to not risk the code bloat or the extra linker work unless there is a benefit that outweighs these costs.

However sometimes I do want to have constexpr values. Thus I introduced:

explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept;

and

constexpr bool  isnormal() const noexcept;

Here is an example use: 9968d47#diff-6c9395b2018418d46bb1c2584aa296d79ab620de7615538c6734648ca6eb71aeR357-R362

test_to_integer()
{
testcase("test_to_integer");
Number x[]{
Copy link
Collaborator

Choose a reason for hiding this comment

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

For this test case in particular, having the inputs and output split into separate arrays makes the test hard to read and maintain. What would you think of making a struct that has both the input and the expected result and making an array of that struct? Maybe something like this:

        struct OneTest
        {
            Number num;
            std::int64_t i;
        };

        // clang-format off
        static constexpr OneTest tests[]{
            // Number                                     int
            {Number{                     0   },                        0},
            // ...
            {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900},
            // ...
        };
        // clang-format on

        for (auto const& test : tests)
        {
            auto j = static_cast<std::int64_t>(test.num);
            BEAST_EXPECT(j == test.i);
        }

You could consider a similar approach to the earlier tests as well, but I think it's a real win here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I've adopted something similar to what you suggest.

operator<(Number const& x, Number const& y) noexcept
{
// If the two amounts have different signs (zero is treated as positive)
// then the comparison is true iff the left is negative.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm lovin' the comments in this section! Thanks!

return one;
if (n == 1)
return f;
auto r = power(f, n / 2);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This power() function is untested from here down. This seems like the interesting stuff? Perhaps some tests could be added to exercise this part?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.

Comment on lines +137 to +147
friend constexpr bool
operator<=(Number const& x, Number const& y) noexcept
{
return !(y < x);
}

friend constexpr bool
operator>=(Number const& x, Number const& y) noexcept
{
return !(x < y);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

These operators are untested. Would probably be good to at least add a couple touch tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added tests.

Comment on lines +221 to +249
inline Number&
Number::operator++()
{
*this += Number{1000000000000000, -15, unchecked{}};
return *this;
}

inline Number
Number::operator++(int)
{
auto x = *this;
++(*this);
return x;
}

inline Number&
Number::operator--()
{
*this -= Number{1000000000000000, -15, unchecked{}};
return *this;
}

inline Number
Number::operator--(int)
{
auto x = *this;
--(*this);
return x;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

These operators are untested.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added tests.

Comment on lines 131 to 136
{
if (exponent_ >= maxExponent)
throw std::overflow_error("Number::normalize 1");
m /= 10;
++exponent_;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

The body of this while loop is untested.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added tests.

Comment on lines +139 to +142
{
*this = Number{};
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

The body of this if is untested.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added tests.


// The Guard class is used to tempoarily add extra digits of
// preicision to an operation. This enables the final result
// to be correctly rounded to the internal precision of Number.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for this comment. It really helps me understand what Guard is doing.

Comment on lines +216 to +217
{Number{-1}, 0, Number{1}},
{Number{5, -1}, 0, Number{0}},
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think these calls are asking for the zeroth root of -1 and 0.5 respectively? If that reading is correct, a couple of non-authoritative) web sites I checked suggest that the zeroth root of any number (possibly excepting 1) is undefined:

So I'm wondering if the better answer for taking the zeroth root of a number is to throw? I'm open to being wrong on this call...

Copy link
Contributor Author

@HowardHinnant HowardHinnant May 17, 2022

Choose a reason for hiding this comment

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

I went with Annex F of the C standard: https://cigix.me/c17#F.10.4.4.p1

pow(−1, ±∞) returns 1.
pow(x, +∞) returns +0 for |x| < 1.

Annex F is consistent with the IEEE floating point standards: https://cigix.me/c17#F.1.p1

I did take the liberty of interpreting 1/0 as +∞.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm great with that call. How would you feel about adding comments to the root(Number, unsigned) and power(Number, unsigned, unsigned) declarations in Number.h that the implementations are consistent with Annex F of the C standard and with IEEE floating point standards?

Comment on lines +267 to +268
{Number{-1}, 1, 0, Number{1}},
{Number{-1, -1}, 1, 0, Number{0}},
Copy link
Collaborator

Choose a reason for hiding this comment

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

Given a little bit of research, I'm thinking that having a 0 in the d parameter of root should be an undefined result and so should throw. Am I misunderstanding?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree that there is room for debate here. But I thought the safest thing to do was follow the established C and IEEE standards.

Copy link
Collaborator

@scottschurr scottschurr left a comment

Choose a reason for hiding this comment

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

First of all, the unit test coverage is outstanding! Thanks for putting in the effort there.

There are a couple of small changes I'd like to see:

  • Rename clip() to something like noiseGate().
  • I'd like to see comments added to root(Number, unsigned) and power(Number, unsigned, unsigned) that they comply with Annex F of the C standard.
  • See whether it makes sense to pass Number const& to the two power functions.

Assuming those are addressed this looks like it's good to go as far as I can see. Nice work!

// Returns f^(n/d)

Number
power(Number f, unsigned n, unsigned d);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Most of the functions in this header pass Number const&. That could be done here too.

// to find the root of the polynomial g(x) = x^d - f

Number
root(Number f, unsigned d);
Copy link
Collaborator

Choose a reason for hiding this comment

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

The implementation of root modifies the passed in f, so there's a good reason to be passing a non-ref Number here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Before we add these functions, I'd like to make sure they are needed. We need to be able to take the square root of a number, and we need to be able to square a number (multiply is likely good enough). I'd like to not add root and ``pow` until we know we need them for sure.

Copy link
Collaborator

Choose a reason for hiding this comment

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

AMM equations require at a minimum root in case of equal weights. pow is required if unequal weights is supported.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@gregtatcam Don't we only need square root, not a general root function?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Correct, just the square root.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the unequal weights case (if we support that), we need the general root function to support the power raise to a general rational function.

// Uses a log_2(n) number of multiplications

Number
power(Number f, unsigned n);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Most of the functions in this header pass Number const&. That could be done here too.

Copy link
Collaborator

Choose a reason for hiding this comment

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

See comment on root.

Choose a reason for hiding this comment

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

雷达啥时候回来

Choose a reason for hiding this comment

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

你们是不是雷达实验室的人

Copy link
Collaborator

@seelabs seelabs left a comment

Choose a reason for hiding this comment

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

I'm a little hesitant to add another numeric class to rippled. Do we really need to add a new one?

Number
power(Number f, unsigned n, unsigned d);

// Return 0 if abs(x) < limit, else returns x
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd like to think of a better name. I agree that this shouldn't be named clip, but noiseGate doesn't really give an intuition for what this function does. I'll try to think of something too. Maybe squelch?

// to find the root of the polynomial g(x) = x^d - f

Number
root(Number f, unsigned d);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Before we add these functions, I'd like to make sure they are needed. We need to be able to take the square root of a number, and we need to be able to square a number (multiply is likely good enough). I'd like to not add root and ``pow` until we know we need them for sure.

// Uses a log_2(n) number of multiplications

Number
power(Number f, unsigned n);
Copy link
Collaborator

Choose a reason for hiding this comment

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

See comment on root.

@HowardHinnant
Copy link
Contributor Author

I've addressed all of @scottschurr's comments. On noiseGate, happy to change it again to whatever the consensus is.

@gregtatcam
Copy link
Collaborator

I'm a little hesitant to add another numeric class to rippled. Do we really need to add a new one?

AMM equations are not like other math ops in the rippled and are neither STAmount or XRPAmount or IOUAmount and can mix all of these types. Moreover, using multiply and divide makes these equations unreadable and error prone. The Number class makes AMM equations easy to implement and understand.

Comment on lines +552 to +555
// This function, and power(Number f, unsigned n, unsigned d)
// treat corner cases such as 0 roots as advised by Annex F of
// the C standard, which itself is consistent with the IEEE
// floating point standards.
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

@haihai423
Copy link

radar的贡献者,雷达还有希望吗

@zhangdongwei123
Copy link

zhangdongwei123 commented May 23, 2022 via email

@zhangdongwei123
Copy link

zhangdongwei123 commented May 23, 2022 via email

@haihai423
Copy link

我也是玩家,你是玩雷达的吗,

@scottschurr
Copy link
Collaborator

@HowardHinnant, should this pull request be closed? It looks like you have a competing pull request: #4192. Your call.

@HowardHinnant
Copy link
Contributor Author

Closed in lieu of #4192.

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.

6 participants