Skip to content

perf: Speed up addition time for drastically different exponents - #7825

Open
ximinez wants to merge 16 commits into
developfrom
ximinez/number-exponents
Open

perf: Speed up addition time for drastically different exponents#7825
ximinez wants to merge 16 commits into
developfrom
ximinez/number-exponents

Conversation

@ximinez

@ximinez ximinez commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

High Level Overview of Change

  • When dropping digits, short circuit the loop if the smaller value loses all significant digits in both the mantissa, and the Guard.
  • Adds unit tests demonstrating some extreme examples.

Context of Change

We received a report that a Json string with an out of range exponent (e.g. 1e2000000000) would successfully bypass Number invariants, and that adding anything to that value would tie up the node for 1-2 seconds as the exponents were brought into sync. The report was completely wrong about the Json parsing. (I added tests to STNumber_test.cpp demonstrating such.) Those values overflow as designed and expected.

However, when I checked two numbers with exponents at the valid opposite extremes (e.g. approximately 1e32767 and 1e-32767), there was in fact a measurable delay.

Before tests, the entire Number test suite took < 1ms (the granularity at which the test harness measured).

I added 16 addition operation tests of such Numbers (one for each combination of MantissaScale and RoundingMode). Together, they took about 80ms. So an average of about 5ms each. Still a pretty insignificant amount of time. Additionally, because the STAmounts, which every STNumber field actually represents, have an exponent range of -96 to 80, any of these extreme values will not get very far in the transaction engine itself. Thus I conclude that the current behavior is not exploitable in any meaningful way. A 5ms addition operation is likely to be dwarfed by the I/O of loading the necessary ledger object for any transaction in which it is relevant.

The fix is relatively straightforward: When shrinking the mantissa and increasing the exponent of one of the operands in an addition operation, if that operand reaches a state where the only possible change would be to increase the exponent, simply set that exponent to the exponent of the other (larger) operand, and exit the loop. The fact that digits were dropped is still preserved, so when rounding up, the result still rounds up to the next representable value.

With this fix, the entire Number test suite is back to running in under 1ms.

ximinez added 4 commits July 15, 2026 18:45
- When dropping digits, short circuit the loop if the smaller value
  loses all significant digits in both the mantissa, and the Guard.
- Adds unit tests demonstrating some extreme examples.

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

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.

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

Copilot AI left a comment

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.

Pull request overview

This PR addresses a performance corner case in xrpl::Number addition when operands have exponents at opposite extremes of the valid range, avoiding long digit-dropping loops when the smaller operand has no remaining significant/recoverable digits. It also adds tests to demonstrate both the performance-sensitive addition scenario and expected overflow behavior when parsing extremely large exponents from JSON.

Changes:

  • Add a Number::Guard fast-path (doDropDigitWithTarget) to jump the exponent to the target once the mantissa is zero and guard digits are no longer recoverable.
  • Add unit tests covering addition with kMaxExponent vs kMinExponent across rounding modes.
  • Add unit tests asserting JSON parsing/normalization overflows for very large exponent strings (e.g. 1e2000000000).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/libxrpl/basics/Number.cpp Adds a guard-based short-circuit during exponent alignment in addition to avoid pathological loop counts.
src/tests/libxrpl/basics/Number.cpp Adds regression tests for extreme-exponent addition behavior (including rounding-mode-specific expectations) and a normalization overflow case.
src/test/protocol/STNumber_test.cpp Refactors repeated exception assertions into a helper and adds overflow tests for huge exponent strings passed via JSON.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

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.

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

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.

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

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.

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

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.

Looks good.

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

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.

Solid logic with thorough testing. Good to ship.

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

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.

This is a focused, well-reasoned performance fix for Number::operator+= that short-circuits the digit-dropping loop once the shrinking operand's mantissa is exhausted and its Guard is full (no more information can be recovered), jumping directly to the target exponent instead of looping once per exponent difference. The fix is bounded correctly: the shortcut only fires when both mantissa == 0 and unrecoverable() (guard full) hold, which is guaranteed to happen within a small, fixed number of iterations (bounded by the mantissa's bit width and the Guard's fixed capacity) regardless of how large the exponent gap is — matching the reported ~1000x speedup with unchanged rounding semantics. The redundant targetExponent > exponent check inside the if (duplicating the debug-only XRPL_ASSERT) is defensive but harmless. Test additions (STNumber_test.cpp refactor to a shared throw-checking lambda, and the new extreme-exponent addition tests across all rounding modes in Number.cpp) look correct and directly validate the fixed behavior, including the Upward-rounding carry case. No correctness, security, or resource issues found in the changed lines.

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

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.

The change adds a short-circuit (doDropDigitWithTarget) to Number::Guard that jumps the shrinking operand's exponent directly to the target once its mantissa is fully zeroed and the guard holds no recoverable digits, avoiding a long O(exponent-delta) loop for additions with widely separated exponents. I traced through the logic: the short-circuit only fires when mantissa==0 and digits_==0, in which case further loop iterations would only push zero digits (no information loss, xbit_ unaffected), so jumping the exponent is behaviorally equivalent to looping. The first loop (trailing-zero shrink) is correctly left using doDropDigit since it can't reach mantissa==0 by construction. The accompanying tests (extreme opposite-exponent addition across all rounding modes, and the STNumber/Number test refactors) appear consistent with this reasoning and the described before/after timing. No correctness, security, or resource issues found in the diff.

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.

2 participants