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

Use <=> operator for base_uint, Issue, and Book: #4411

Merged
merged 1 commit into from
Mar 15, 2023

Conversation

drlongle
Copy link
Contributor

@drlongle drlongle commented Feb 7, 2023

High Level Overview of Change

This PR resolves issue 2525. The PR implements the operator==and the operator<=> (aka the spaceship operator) in base_uint, Issue, and Book. C++20-compliant compilers automatically provide the remaining comparison operators (e.g. operator<, operator<=, ...). I also remove the function compare() because it is no longer needed. The PR provides the same semantics as the existing code. All unit tests pass. I also added some unit tests to gain further confidence.

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)
  • Technical debt
  • 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

@drlongle
Copy link
Contributor Author

drlongle commented Feb 7, 2023

It seems that std::lexicographical_compare_three_way is not yet supported by Xcode. I'll look into alternatives.

@drlongle
Copy link
Contributor Author

drlongle commented Feb 8, 2023

PR is ready for review. It would be nice to use std::is_eq(), std::is_neq() and std::lexicographical_compare_three_way(). Unfortunately, these functions are not yet suppored on MacOS. I use std::weak_ordering::equivalent and std::mismatch() instead.

Copy link
Contributor

@nbougalis nbougalis left a comment

Choose a reason for hiding this comment

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

Nice. I think the code can be further improved and cleaned up. I left some comments.

src/ripple/basics/base_uint.h Outdated Show resolved Hide resolved
Comment on lines 68 to 74
template <std::size_t Bits, typename Tag>
class base_uint;

template <std::size_t Bits, typename Tag>
[[nodiscard]] constexpr bool
operator==(base_uint<Bits, Tag> const& lhs, base_uint<Bits, Tag> const& rhs);

Copy link
Contributor

Choose a reason for hiding this comment

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

All this is unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My intention was to be able to compare the arrays data_ with std::uint32_t data elements instead of using std::mismatch() with unsigned char data elements. It's probably an overkill. I revert this change and take your suggested code.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I see what you were trying to do. It makes sense, but you need to be careful (and you were). This trick will ONLY work for equality/inequality comparisons, because the data_ array is stored in big endian order, which on platforms like the x86 is NOT the native endian order. Relational comparisons (gt/gte/lt/lte) break. Consider, for example, the following two 32-bit unsigned values:

a = 256, b = 1

If you convert them into big endian they look like

x = 65536, y = 16777216

Notice what happened... a > b but x < y. Oops!

I think that having the "bespoke" operator== is fine although I doubt there are any performance gains to be had. But you should definitely consider adding a comment explaining that this is safe, despite the fact that the individual limbs (the entries in the data_ array) are in big endian, because we are comparing for equality/inequality.

The existing compare function CANNOT do that trick and MUST use std::mismatch. It could also use a comment too, explaining that this works because the pointer returned from cbegin is a byte-based view of the data_ which is actually in big-endian order.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right. This is quite tricky. I use std::mismatch for now.

src/ripple/basics/base_uint.h Outdated Show resolved Hide resolved
Comment on lines 70 to 77
[[nodiscard]] bool
operator==(Book const& lhs, Book const& rhs);
bool
operator!=(Book const& lhs, Book const& rhs);
/** @} */

/** Strict weak ordering. */
/** @{ */
bool
operator<(Book const& lhs, Book const& rhs);
bool
operator>(Book const& lhs, Book const& rhs);
bool
operator>=(Book const& lhs, Book const& rhs);
bool
operator<=(Book const& lhs, Book const& rhs);
[[nodiscard]] std::weak_ordering
operator<=>(Book const& lhs, Book const& rhs);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can just use defaulted comparisons for these. Simply add the following inside the declaration of Book as a public and you automatically get all six relational operators:

auto operator<=>(Bool const&) const = default;

NOTE, HOWEVER, THAT THIS HAS A RISK:* first, the order of the member variables matters (since the default comparison goes through each member in order) and if any additional variables are added, it might cause subtle breakage of comparisons.

With that in mind, I'm not sure it's worth defaulting, but just pointing it out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right. We could default here but the operator<=> becomes a bit fragile as you pointed out.

I would not default unless you insist. Please let me know.

src/ripple/protocol/impl/Issue.cpp Outdated Show resolved Hide resolved
Copy link
Contributor

@nbougalis nbougalis left a comment

Choose a reason for hiding this comment

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

Looks good. Left some small comments.

src/ripple/app/tx/impl/details/NFTokenUtils.cpp Outdated Show resolved Hide resolved
src/ripple/app/tx/impl/details/NFTokenUtils.cpp Outdated Show resolved Hide resolved
src/ripple/basics/base_uint.h Outdated Show resolved Hide resolved
src/ripple/basics/base_uint.h Outdated Show resolved Hide resolved
src/ripple/protocol/impl/Book.cpp Outdated Show resolved Hide resolved
src/ripple/protocol/impl/Issue.cpp Outdated Show resolved Hide resolved
src/ripple/protocol/impl/Issue.cpp Outdated Show resolved Hide resolved
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.

Nice work! As far as I can see everything should work correctly.

I left a number of comments for things I think might be minor improvements. Specifically, I think you're working too hard when you use the stored result of the <=> operator. You don't need to use constants like std::weak_ordering::equivalent. It is sufficient (and easier to read) to compare to literal 0.

Comparison to 0 is how the compiler rewrites comparison expressions to use the <=> operator. See section 2.2.3 of the original spaceship paper: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0515r3.pdf

I think that's the most important change to consider. I'm also open to being told I'm wrong. But I think it's useful to get this right early on, since this is the first introduction of the <=> operator to this code base. People will be copying the example that you set here.

Comment on lines 149 to 152
// FIXME: use std::is_eq and std::is_gt once support is
// added to MacOS.
auto const relation{(id & nft::pageMask) <=> cmp};
if (relation == std::strong_ordering::equivalent)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This works. So no change necessary. But I think you're working harder than you need to. Consider the rewrite rules that C++ follows with the <=> operator:

(a <=> b) @ 0

So I don't think we need std::strong_ordering::equivalent or std::is_eq or std::is_gt. All we need is 0. I suggest rewriting the comparison like this:

                auto const relation{(id & nft::pageMask) <=> cmp};
                if (relation == 0)

I think that allows you to remove the FIXME. I think it's also easier to read.

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 for this suggestion. It makes the code clean and concise.


else if (relation > 0)
if (relation == std::strong_ordering::greater)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar to the above comment, you can simplify this to

if (relation > 0)

with no loss of generality.

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. Fixed.

Comment on lines 237 to 241
// FIXME: use std::is_eq and std::is_lt once support is
// added to MacOS.
if (auto const c{(a & nft::pageMask) <=> (b & nft::pageMask)};
c != std::strong_ordering::equivalent)
return c == std::strong_ordering::less;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, this works, but I think you're working harder than necessary. A more minimal change might look like this:

    if (auto const lowBitsCmp = (a & nft::pageMask) <=> (b & nft::pageMask);
        lowBitsCmp != 0)
        return lowBitsCmp < 0;

No FIXME comment necessary.

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. Fixed.

@@ -549,103 +550,77 @@ using uint160 = base_uint<160>;
using uint256 = base_uint<256>;

template <std::size_t Bits, class Tag>
inline int
compare(base_uint<Bits, Tag> const& a, base_uint<Bits, Tag> const& b)
[[nodiscard]] inline constexpr std::strong_ordering
Copy link
Collaborator

Choose a reason for hiding this comment

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

I really like the [[nodiscard]] and constexpr. Nice!

Comment on lines 570 to 575
// a > b
if (*ret.first > *ret.second)
return 1;
return std::strong_ordering::greater;

// a < b
return -1;
}

template <std::size_t Bits, class Tag>
inline bool
operator<(base_uint<Bits, Tag> const& a, base_uint<Bits, Tag> const& b)
{
return compare(a, b) < 0;
}

template <std::size_t Bits, class Tag>
inline bool
operator<=(base_uint<Bits, Tag> const& a, base_uint<Bits, Tag> const& b)
{
return compare(a, b) <= 0;
}

template <std::size_t Bits, class Tag>
inline bool
operator>(base_uint<Bits, Tag> const& a, base_uint<Bits, Tag> const& b)
{
return compare(a, b) > 0;
}

template <std::size_t Bits, class Tag>
inline bool
operator>=(base_uint<Bits, Tag> const& a, base_uint<Bits, Tag> const& b)
{
return compare(a, b) >= 0;
}

template <std::size_t Bits, class Tag>
inline bool
operator==(base_uint<Bits, Tag> const& a, base_uint<Bits, Tag> const& b)
{
return compare(a, b) == 0;
return std::strong_ordering::less;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This works fine. If you want you could reduce it a bit like this:

    return (*ret.first > *ret.second) ? std::strong_ordering::greater
                                      : std::strong_ordering::less;

That reduces from two returns to one. But your choice.

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. Fixed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Wait what? You can’t reduce lines 571-575 down to a single return.

Am I not understanding the suggestion 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.

The code is now like below.

    auto const ret = std::mismatch(lhs.cbegin(), lhs.cend(), rhs.cbegin());

    // a == b
    if (ret.first == lhs.cend())
        return std::strong_ordering::equivalent;

    return (*ret.first > *ret.second) ? std::strong_ordering::greater
                                      : std::strong_ordering::less;

We can use a ternary operator because we know that a != b. If you want to revert this change, let me know.

{"123456789abc", "aaaaaaaaaaaa"},
{"999999999999", "aaaaaaaaaaaa"}};

for (auto& arg : test_args)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could be auto const& arg.

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. Fixed.


for (auto& arg : test_args)
{
ripple::base_uint<96> u{arg.first}, v{arg.second};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could be

ripple::base_uint<96> const u{arg.first}, v{arg.second};

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. Fixed.

Comment on lines 96 to 101
std::vector<std::pair<std::string, std::string>> test_args{
{"0123456789ab", "123456789abc"},
{"010101010101", "111111111111"},
{"000000000000", "010101010101"},
{"123456789abc", "aaaaaaaaaaaa"},
{"999999999999", "aaaaaaaaaaaa"}};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, we're only exercising half the bits in the uint_base<96>. Consider this alternative:

            static constexpr std::array<
                std::pair<std::string_view, std::string_view>,
                6>
                test_args{{
                    {"000000000000000000000000", "000000000000000000000001"},
                    {"000000000000000000000000", "ffffffffffffffffffffffff"},
                    {"0123456789ab0123456789ab", "123456789abc123456789abc"},
                    {"555555555555555555555555", "55555555555a555555555555"},
                    {"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"},
                    {"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"},
                }};

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. Fixed.

Comment on lines +556 to +562
// This comparison might seem wrong on a casual inspection because it
// compares data internally stored as std::uint32_t byte-by-byte. But
// note that the underlying data is stored in big endian, even if the
// plaform is little endian. This makes the comparison correct.
//
// FIXME: use std::lexicographical_compare_three_way once support is
// added to MacOS.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice comment!

Comment on lines 60 to 61
static_assert(std::is_copy_constructible<test96>::value, "");
static_assert(std::is_copy_assignable<test96>::value, "");
Copy link
Collaborator

Choose a reason for hiding this comment

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

These are pre-C++17 static_asserts. While you're in the file would you consider removing the closing , ""? Thanks.

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. Fixed.

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.

Looks good to me! I spotted one more possible change, but it's not critical. So I'm approving. Feel free to make that change if you want, even with this approval.

src/ripple/protocol/Book.h Outdated Show resolved Hide resolved
src/test/basics/base_uint_test.cpp Show resolved Hide resolved
@scottschurr
Copy link
Collaborator

When you squash all of these commits, try to keep the first line of the commit message under 60 characters or so. You might consider something like this for your commit message:

Use <=> operator for base_uint, Issue, and Book (#4411):

Fixes #2525.

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.

👍 Looks great! Nice work.

@drlongle drlongle force-pushed the retire-compare-func-for-base-uint branch from e873b55 to f7daa6d Compare February 17, 2023 17:04
@intelliot
Copy link
Collaborator

@drlongle if you feel this is ready to merge, you can put the Passed label on it

@drlongle drlongle added the Passed Passed code review & PR owner thinks it's ready to merge. Perf sign-off may still be required. label Feb 19, 2023
@intelliot intelliot added this to the 1.10.1 milestone Feb 21, 2023
@intelliot intelliot merged commit 84cde3c into XRPLF:develop Mar 15, 2023
ximinez added a commit to ximinez/rippled that referenced this pull request Mar 15, 2023
* upstream/develop:
  Rectify the import paths of boost/iterator: (XRPLF#4293)
  Allow port numbers be be specified with a colon: (XRPLF#4328)
  Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411)
  Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
ximinez added a commit to ximinez/rippled that referenced this pull request Mar 15, 2023
* upstream/develop:
  Rectify the import paths of boost/iterator: (XRPLF#4293)
  Allow port numbers be be specified with a colon: (XRPLF#4328)
  Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411)
  Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
ximinez added a commit to ximinez/rippled that referenced this pull request Mar 15, 2023
* upstream/develop:
  Rectify the import paths of boost/iterator: (XRPLF#4293)
  Allow port numbers be be specified with a colon: (XRPLF#4328)
  Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411)
  Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
ximinez added a commit to ximinez/rippled that referenced this pull request Mar 15, 2023
* upstream/develop:
  Rectify the import paths of boost/iterator: (XRPLF#4293)
  Allow port numbers be be specified with a colon: (XRPLF#4328)
  Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411)
  Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
ximinez added a commit to ximinez/rippled that referenced this pull request Mar 15, 2023
* upstream/develop:
  Rectify the import paths of boost/iterator: (XRPLF#4293)
  Allow port numbers be be specified with a colon: (XRPLF#4328)
  Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411)
  Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
ximinez added a commit to ximinez/rippled that referenced this pull request Mar 15, 2023
…ctionality

* upstream/develop:
  Rectify the import paths of boost/iterator: (XRPLF#4293)
  Allow port numbers be be specified with a colon: (XRPLF#4328)
  Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411)
  Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
ximinez added a commit to ximinez/rippled that referenced this pull request Mar 15, 2023
…tpage

* upstream/develop:
  Rectify the import paths of boost/iterator: (XRPLF#4293)
  Allow port numbers be be specified with a colon: (XRPLF#4328)
  Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411)
  Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
@intelliot intelliot changed the title Replace compare() with the three-way comparison operator in base_uint, Issue and Book Use <=> operator for base_uint, Issue, and Book: Jun 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Passed Passed code review & PR owner thinks it's ready to merge. Perf sign-off may still be required.
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Revisit need for compare in base_uint.h
4 participants