Compare commits

...

3 Commits

Author SHA1 Message Date
Valentin Balaschenko
a86a90edd7 avoid expensive assignement 2026-02-04 13:21:24 +00:00
Valentin Balaschenko
ed13af878e refactor 2026-02-03 17:54:26 +00:00
Valentin Balaschenko
3c1505a29d avoid repeated normalizations 2026-02-03 17:26:51 +00:00
4 changed files with 185 additions and 20 deletions

View File

@@ -45,6 +45,9 @@ public:
static int const cMinOffset = -96;
static int const cMaxOffset = 80;
// The -100 is used to allow 0 to sort less than small positive values
// which will have a large negative exponent.
static int const cZeroOffset = -100;
// Maximum native value supported by the code
constexpr static std::uint64_t cMinValue = 1'000'000'000'000'000ull;
@@ -524,7 +527,11 @@ STAmount::fromNumber(A const& a, Number const& number)
auto const [mantissa, exponent] = working.normalizeToRange(cMinValue, cMaxValue);
return STAmount{asset, mantissa, exponent, negative};
// normalizeToRange produces values in canonical mantissa range [cMinValue, cMaxValue],
// but may produce out-of-range exponents for overflow/underflow cases.
// Use the regular constructor - canonicalize() will detect already-normalized mantissa
// and skip redundant scaling loops, while still handling overflow/underflow.
return STAmount{asset, static_cast<std::uint64_t>(mantissa), exponent, negative};
}
inline void
@@ -537,9 +544,7 @@ STAmount::negate()
inline void
STAmount::clear()
{
// The -100 is used to allow 0 to sort less than a small positive values
// which have a negative exponent.
mOffset = integral() ? 0 : -100;
mOffset = integral() ? 0 : cZeroOffset;
mValue = 0;
mIsNegative = false;
}

View File

@@ -38,7 +38,10 @@ Number::getround()
Number::rounding_mode
Number::setround(rounding_mode mode)
{
return std::exchange(mode_, mode);
auto const old = mode_;
if (old != mode)
mode_ = mode;
return old;
}
MantissaRange::mantissa_scale
@@ -52,6 +55,8 @@ Number::setMantissaScale(MantissaRange::mantissa_scale scale)
{
if (scale != MantissaRange::small && scale != MantissaRange::large)
LogicError("Unknown mantissa scale");
if (range_.get().scale == scale)
return;
range_ = scale == MantissaRange::small ? smallRange : largeRange;
}

View File

@@ -866,34 +866,44 @@ STAmount::canonicalize()
if (mValue == 0)
{
mOffset = -100;
mOffset = cZeroOffset;
mIsNegative = false;
return;
}
while ((mValue < cMinValue) && (mOffset > cMinOffset))
// Fast path: if mantissa is already in canonical range, skip scaling loops.
// This handles values from normalizeToRange that only need overflow/underflow checks.
bool const mantissaCanonical = (mValue >= cMinValue) && (mValue <= cMaxValue);
if (!mantissaCanonical)
{
mValue *= 10;
--mOffset;
}
while (mValue > cMaxValue)
{
if (mOffset >= cMaxOffset)
Throw<std::runtime_error>("value overflow");
mValue /= 10;
++mOffset;
// Mantissa needs normalization
while ((mValue < cMinValue) && (mOffset > cMinOffset))
{
mValue *= 10;
--mOffset;
}
while (mValue > cMaxValue)
{
if (mOffset >= cMaxOffset)
Throw<std::runtime_error>("value overflow");
mValue /= 10;
++mOffset;
}
}
// Check for underflow (applies whether we scaled or not)
if ((mOffset < cMinOffset) || (mValue < cMinValue))
{
mValue = 0;
mIsNegative = false;
mOffset = -100;
mOffset = cZeroOffset;
return;
}
// Check for overflow (applies whether we scaled or not)
if (mOffset > cMaxOffset)
Throw<std::runtime_error>("value overflow");
@@ -903,7 +913,7 @@ STAmount::canonicalize()
XRPL_ASSERT(
(mValue == 0) || ((mOffset >= cMinOffset) && (mOffset <= cMaxOffset)),
"xrpl::STAmount::canonicalize : offset inside range");
XRPL_ASSERT((mValue != 0) || (mOffset != -100), "xrpl::STAmount::canonicalize : value or offset set");
XRPL_ASSERT((mValue != 0) || (mOffset == cZeroOffset), "xrpl::STAmount::canonicalize : value or offset set");
}
void

View File

@@ -1,7 +1,9 @@
#include <test/jtx.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/XRPAmount.h>
@@ -1143,6 +1145,148 @@ public:
}
}
void
testNumberConversion()
{
testcase("Number to STAmount conversions");
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
NumberSO stNumberSO{true};
// Test zero conversion
{
Number const zero{};
STAmount const result{usd, zero};
BEAST_EXPECT(result.mantissa() == 0);
BEAST_EXPECT(result.exponent() == STAmount::cZeroOffset);
BEAST_EXPECT(!result.negative());
}
// Test positive zero
{
Number const zero{0, 0};
STAmount const result{usd, zero};
BEAST_EXPECT(result.mantissa() == 0);
BEAST_EXPECT(result.exponent() == STAmount::cZeroOffset);
}
// Test negative zero (should become positive zero)
{
Number const negZero{-0, 0};
STAmount const result{usd, negZero};
BEAST_EXPECT(result.mantissa() == 0);
BEAST_EXPECT(!result.negative());
}
// Test minimum positive IOU amount
{
Number const minPos{STAmount::cMinValue, STAmount::cMinOffset};
STAmount const result{usd, minPos};
BEAST_EXPECT(result.mantissa() == STAmount::cMinValue);
BEAST_EXPECT(result.exponent() == STAmount::cMinOffset);
BEAST_EXPECT(!result.negative());
}
// Test maximum positive IOU amount
{
Number const maxPos{STAmount::cMaxValue, STAmount::cMaxOffset};
STAmount const result{usd, maxPos};
BEAST_EXPECT(result.mantissa() == STAmount::cMaxValue);
BEAST_EXPECT(result.exponent() == STAmount::cMaxOffset);
BEAST_EXPECT(!result.negative());
}
// Test negative amounts
{
Number const neg{-static_cast<std::int64_t>(STAmount::cMinValue), STAmount::cMinOffset};
STAmount const result{usd, neg};
BEAST_EXPECT(result.mantissa() == STAmount::cMinValue);
BEAST_EXPECT(result.exponent() == STAmount::cMinOffset);
BEAST_EXPECT(result.negative());
}
// Test value requiring scale up (mantissa too small)
{
Number const small{1000000000000000ull / 10, -95}; // Will scale up
STAmount const result{usd, small};
BEAST_EXPECT(result.mantissa() >= STAmount::cMinValue);
BEAST_EXPECT(result.mantissa() <= STAmount::cMaxValue);
BEAST_EXPECT(result.exponent() >= STAmount::cMinOffset);
BEAST_EXPECT(result.exponent() <= STAmount::cMaxOffset);
}
// Test value requiring scale down (mantissa too large)
{
Number const large{9999999999999999ull * 10, 79}; // Will scale down
STAmount const result{usd, large};
BEAST_EXPECT(result.mantissa() >= STAmount::cMinValue);
BEAST_EXPECT(result.mantissa() <= STAmount::cMaxValue);
BEAST_EXPECT(result.exponent() >= STAmount::cMinOffset);
BEAST_EXPECT(result.exponent() <= STAmount::cMaxOffset);
}
// Test boundary mantissa values
{
Number const atMin{STAmount::cMinValue, 0};
STAmount const result{usd, atMin};
BEAST_EXPECT(result.mantissa() == STAmount::cMinValue);
}
{
Number const atMax{STAmount::cMaxValue, 0};
STAmount const result{usd, atMax};
BEAST_EXPECT(result.mantissa() == STAmount::cMaxValue);
}
// Test typical amounts
{
Number const typical{1234567890123456ull, -10};
STAmount const result{usd, typical};
BEAST_EXPECT(result.mantissa() >= STAmount::cMinValue);
BEAST_EXPECT(result.mantissa() <= STAmount::cMaxValue);
BEAST_EXPECT(result.exponent() >= STAmount::cMinOffset);
BEAST_EXPECT(result.exponent() <= STAmount::cMaxOffset);
}
// Test round-trip conversion (Number -> STAmount -> Number)
{
Number const original{5000000000000000ull, 5};
STAmount const st{usd, original};
Number const recovered{st};
BEAST_EXPECT(original == recovered);
}
// Test various exponents
for (int exp = STAmount::cMinOffset; exp <= STAmount::cMaxOffset; exp += 10)
{
Number const n{STAmount::cMinValue, exp};
STAmount const result{usd, n};
BEAST_EXPECT(result.mantissa() >= STAmount::cMinValue);
BEAST_EXPECT(result.mantissa() <= STAmount::cMaxValue);
BEAST_EXPECT(result.exponent() >= STAmount::cMinOffset);
BEAST_EXPECT(result.exponent() <= STAmount::cMaxOffset);
}
// Test both mantissa scales (if applicable)
{
// Small mantissa scale test
NumberMantissaScaleGuard guard{MantissaRange::small};
Number const n{5000000000000000ull, 5};
STAmount const result{usd, n};
BEAST_EXPECT(result.mantissa() >= STAmount::cMinValue);
BEAST_EXPECT(result.mantissa() <= STAmount::cMaxValue);
}
{
// Large mantissa scale test
NumberMantissaScaleGuard guard{MantissaRange::large};
Number const n{5000000000000000ull, 5};
STAmount const result{usd, n};
BEAST_EXPECT(result.mantissa() >= STAmount::cMinValue);
BEAST_EXPECT(result.mantissa() <= STAmount::cMaxValue);
}
}
//--------------------------------------------------------------------------
void
@@ -1157,6 +1301,7 @@ public:
testParseJson();
testConvertXRP();
testConvertIOU();
testNumberConversion();
testCanAddXRP();
testCanAddIOU();
testCanAddMPT();