From f924b3fac16ac35ad000be0c6a4f1e1cf9d2c85c Mon Sep 17 00:00:00 2001 From: Geert Bosch Date: Thu, 31 Mar 2016 15:06:35 -0400 Subject: SERVER-19703 Add a few new Decimal128 methods/constructors for usage by KeyString Make conversions explicit and allow control over precision in conversion from double. Update uses to the new interface. --- src/mongo/bson/bsonelement.cpp | 6 +- src/mongo/bson/bsonelement.h | 2 +- src/mongo/bson/mutable/document.cpp | 4 +- src/mongo/db/pipeline/value.cpp | 4 +- src/mongo/platform/decimal128.cpp | 49 ++++++------- src/mongo/platform/decimal128.h | 118 +++++++++++++++++++++++++++--- src/mongo/platform/decimal128_dummy.cpp | 6 +- src/mongo/platform/decimal128_test.cpp | 44 ++++++++++- src/mongo/scripting/mozjs/valuewriter.cpp | 3 +- src/mongo/util/safe_num.cpp | 12 +-- 10 files changed, 196 insertions(+), 52 deletions(-) (limited to 'src') diff --git a/src/mongo/bson/bsonelement.cpp b/src/mongo/bson/bsonelement.cpp index 64a2d35d8ed..7c934fb8c81 100644 --- a/src/mongo/bson/bsonelement.cpp +++ b/src/mongo/bson/bsonelement.cpp @@ -31,8 +31,8 @@ #include "mongo/bson/bsonelement.h" -#include #include +#include #include "mongo/base/compare_numbers.h" #include "mongo/base/data_cursor.h" @@ -1051,7 +1051,9 @@ size_t BSONElement::Hasher::operator()(const BSONElement& elem) const { case mongo::NumberDecimal: { const Decimal128 dcml = elem.numberDecimal(); - if (dcml.toAbs().isGreater(Decimal128(std::numeric_limits::max())) && + if (dcml.toAbs().isGreater(Decimal128(std::numeric_limits::max(), + Decimal128::kRoundTo34Digits, + Decimal128::kRoundTowardZero)) && !dcml.isInfinite() && !dcml.isNaN()) { // Normalize our decimal to force equivalent decimals // in the same cohort to hash to the same value diff --git a/src/mongo/bson/bsonelement.h b/src/mongo/bson/bsonelement.h index b0b2f05aa1c..5391e622b0e 100644 --- a/src/mongo/bson/bsonelement.h +++ b/src/mongo/bson/bsonelement.h @@ -711,7 +711,7 @@ inline Decimal128 BSONElement::numberDecimal() const { case NumberDecimal: return _numberDecimal(); default: - return 0; + return Decimal128::kNormalizedZero; } } diff --git a/src/mongo/bson/mutable/document.cpp b/src/mongo/bson/mutable/document.cpp index 85cfa23ce53..612baf1a285 100644 --- a/src/mongo/bson/mutable/document.cpp +++ b/src/mongo/bson/mutable/document.cpp @@ -1900,7 +1900,7 @@ Status Element::setValueSafeNum(const SafeNum value) { case mongo::NumberDouble: return setValueDouble(value._value.doubleVal); case mongo::NumberDecimal: - return setValueDecimal(value._value.decimalVal); + return setValueDecimal(Decimal128(value._value.decimalVal)); default: return Status(ErrorCodes::UnsupportedFormat, "Don't know how to handle unexpected SafeNum type"); @@ -2564,7 +2564,7 @@ Element Document::makeElementSafeNum(StringData fieldName, SafeNum value) { case mongo::NumberDouble: return makeElementDouble(fieldName, value._value.doubleVal); case mongo::NumberDecimal: - return makeElementDecimal(fieldName, value._value.decimalVal); + return makeElementDecimal(fieldName, Decimal128(value._value.decimalVal)); default: // Return an invalid element to indicate that we failed. return end(); diff --git a/src/mongo/db/pipeline/value.cpp b/src/mongo/db/pipeline/value.cpp index 704f922755b..cc35067b9da 100644 --- a/src/mongo/db/pipeline/value.cpp +++ b/src/mongo/db/pipeline/value.cpp @@ -822,7 +822,9 @@ void Value::hash_combine(size_t& seed) const { case mongo::NumberDecimal: { const Decimal128 dcml = getDecimal(); - if (dcml.toAbs().isGreater(Decimal128(std::numeric_limits::max())) && + if (dcml.toAbs().isGreater(Decimal128(std::numeric_limits::max(), + Decimal128::kRoundTo34Digits, + Decimal128::kRoundTowardZero)) && !dcml.isInfinite() && !dcml.isNaN()) { // Normalize our decimal to force equivalent decimals // in the same cohort to hash to the same value diff --git a/src/mongo/platform/decimal128.cpp b/src/mongo/platform/decimal128.cpp index 8ec4cc924b1..ddaa381b367 100644 --- a/src/mongo/platform/decimal128.cpp +++ b/src/mongo/platform/decimal128.cpp @@ -30,10 +30,10 @@ #include #include +#include #include #include #include -#include // The Intel C library typedefs wchar_t, but it is a distinct fundamental type // in C++, so we #define _WCHAR_T here to prevent the library from trying to typedef. #define _WCHAR_T @@ -202,13 +202,16 @@ Decimal128::Decimal128(std::int64_t int64Value) * In the worst case, proven by the above error analysis, we only need to * requantize once to yield exactly 15 decimal digits of precision. */ -Decimal128::Decimal128(double doubleValue, RoundingMode roundMode) { +Decimal128::Decimal128(double doubleValue, + RoundingPrecision roundPrecision, + RoundingMode roundMode) { BID_UINT128 convertedDoubleValue; std::uint32_t throwAwayFlag = 0; convertedDoubleValue = binary64_to_bid128(doubleValue, roundMode, &throwAwayFlag); // If the original number was zero, infinity, or NaN, there's no need to quantize - if (doubleValue == 0.0 || std::isinf(doubleValue) || std::isnan(doubleValue)) { + if (doubleValue == 0.0 || std::isinf(doubleValue) || std::isnan(doubleValue) || + roundPrecision == kRoundTo34Digits) { _value = libraryTypeToValue(convertedDoubleValue); return; } @@ -629,37 +632,33 @@ const std::uint64_t t17hi32 = t17 >> 32; // t17hi32*t17hi32 + 2*t17hi32*t17lo32 + t17lo32*t17lo32 where the 2nd term // is shifted right by 32 and the 3rd term by 64 (which effectively drops the 3rd term) const std::uint64_t t34hi64 = t17hi32 * t17hi32 + (((t17hi32 * t17lo32) >> 31)); - -// Get the max exponent for a decimal128 (including the bias) -const std::uint64_t maxBiasedExp = 6143 + 6144; -// Get the binary representation of the negative sign bit -const std::uint64_t negativeSignBit = 1ull << 63; +static_assert(t34hi64 == 0x1ed09bead87c0, ""); +static_assert(t34lo64 == 0x378d8e63ffffffff, ""); } // namespace -// The low bits of the largest positive number are all 9s (t34lo64) and -// the highest are t32hi64 added to the max exponent shifted over 49. -// The exponent is placed at 49 because 64 bits - 1 sign bit - 14 exponent bits = 49 -const Decimal128 Decimal128::kLargestPositive(Decimal128::Value({t34lo64, - (maxBiasedExp << 49) + t34hi64})); -// The smallest positive decimal is 1 with the largest negative exponent of 0 (biased -6176) -const Decimal128 Decimal128::kSmallestPositive(Decimal128::Value({1ull, 0ull})); +// (t34hi64 << 64) + t34lo64 == 1e34 - 1 +const Decimal128 Decimal128::kLargestPositive(0, Decimal128::kMaxBiasedExponent, t34hi64, t34lo64); +// The smallest positive decimal is 1 with the largest negative exponent of 0 (biased) +const Decimal128 Decimal128::kSmallestPositive(0, 0, 0, 1); // Add a sign bit to the largest and smallest positive to get their corresponding negatives -const Decimal128 Decimal128::kLargestNegative( - Decimal128::Value({t34lo64, (maxBiasedExp << 49) + t34hi64 + negativeSignBit})); -const Decimal128 Decimal128::kSmallestNegative(Decimal128::Value({1ull, 0ull + negativeSignBit})); -// Get the reprsentation of 0 with the largest negative exponent +const Decimal128 Decimal128::kLargestNegative(1, Decimal128::kMaxBiasedExponent, t34hi64, t34lo64); +const Decimal128 Decimal128::kSmallestNegative(1, 0, 0, 1); + +// Get the representation of 0 (0E0). +const Decimal128 Decimal128::kNormalizedZero(Decimal128::Value( + {0, static_cast(Decimal128::kExponentBias) << Decimal128::kExponentFieldPos})); + +// Get the representation of 0 with the most negative exponent const Decimal128 Decimal128::kLargestNegativeExponentZero(Decimal128::Value({0ull, 0ull})); // Shift the format of the combination bits to the right position to get Inf and NaN -// +Inf = 0111 1000 ... ... = 0x78 ... ... -// +NaN = 0111 1100 ... ... = 0x7c ... ... +// +Inf = 0111 1000 ... ... = 0x78 ... ..., -Inf = 1111 1000 ... ... = 0xf8 ... ... +// +NaN = 0111 1100 ... ... = 0x7c ... ..., -NaN = 1111 1100 ... ... = 0xfc ... ... const Decimal128 Decimal128::kPositiveInfinity(Decimal128::Value({0ull, 0x78ull << 56})); -const Decimal128 Decimal128::kNegativeInfinity( - Decimal128::Value({0ull, (0x78ull << 56) + negativeSignBit})); +const Decimal128 Decimal128::kNegativeInfinity(Decimal128::Value({0ull, 0xf8ull << 56})); const Decimal128 Decimal128::kPositiveNaN(Decimal128::Value({0ull, 0x7cull << 56})); -const Decimal128 Decimal128::kNegativeNaN(Decimal128::Value({0ull, - (0x7cull << 56) + negativeSignBit})); +const Decimal128 Decimal128::kNegativeNaN(Decimal128::Value({0ull, 0xfcull << 56})); std::ostream& operator<<(std::ostream& stream, const Decimal128& value) { return stream << value.toString(); diff --git a/src/mongo/platform/decimal128.h b/src/mongo/platform/decimal128.h index e9112190bbb..7e6d72a0459 100644 --- a/src/mongo/platform/decimal128.h +++ b/src/mongo/platform/decimal128.h @@ -35,6 +35,8 @@ #include "mongo/config.h" +#include "mongo/util/assert_util.h" + namespace mongo { /** @@ -69,6 +71,7 @@ public: static const Decimal128 kLargestNegative; static const Decimal128 kSmallestNegative; + static const Decimal128 kNormalizedZero; // zero with exponent 0 static const Decimal128 kLargestNegativeExponentZero; static const Decimal128 kPositiveInfinity; @@ -76,6 +79,11 @@ public: static const Decimal128 kPositiveNaN; static const Decimal128 kNegativeNaN; + static const uint32_t kMaxBiasedExponent = 6143 + 6144; + // Biased exponent of a Decimal128 with least significant digit in the units place + static const int32_t kExponentBias = 6143 + 33; + static const uint32_t kInfinityExponent = kMaxBiasedExponent + 1; // internal convention only + /** * This struct holds the raw data for IEEE 754-2008 data types */ @@ -92,6 +100,12 @@ public: kRoundTiesToAway = 4 }; + /** + * Indicates if constructing a Decimal128 from a double should round the double to 15 digits + * (so the conversion will correctly round-trip decimals), or round to the full 34 digits. + */ + enum RoundingPrecision { kRoundTo15Digits = 0, kRoundTo34Digits = 1 }; + /** * The signaling flag enum determines the signaling nature of a decimal operation. * The values of these flags are defined in the Intel RDFP math library. @@ -118,28 +132,47 @@ public: return ((signalingFlags & f) != 0u); } - Decimal128() = default; + /** + * Construct a 0E0 valued Decimal128. + */ + Decimal128() : _value(kNormalizedZero._value) {} /** * This constructor takes in a raw decimal128 type, which consists of two * uint64_t's. This class performs an endian check on the system to ensure * that the Value.high64 represents the higher 64 bits. */ - Decimal128(Decimal128::Value dec128Value) : _value(dec128Value) {} + explicit Decimal128(Decimal128::Value dec128Value) : _value(dec128Value) {} - Decimal128(std::int32_t int32Value); - Decimal128(std::int64_t int64Value); + /** + * Constructs a Decimal128 from parts, dealing with proper encoding of the combination field. + * Assumes that the value will be inside the valid range of finite values. (No NaN/Inf, etc.) + */ + Decimal128(uint64_t sign, uint64_t exponent, uint64_t coefficientHigh, uint64_t coefficientLow) + : _value( + Value{coefficientLow, + (sign << kSignFieldPos) | (exponent << kExponentFieldPos) | coefficientHigh}) { + dassert(coefficientHigh < 0x1ed09bead87c0 || + (coefficientHigh == 0x1ed09bead87c0 && coefficientLow == 0x378d8e63ffffffff)); + dassert(exponent == getBiasedExponent()); + } + + explicit Decimal128(std::int32_t int32Value); + explicit Decimal128(std::int64_t int64Value); /** - * This constructor takes a double and constructs a Decimal128 object - * given a roundMode with a fixed precision of 15. Doubles can only - * properly represent a decimal precision of 15-17 digits. + * This constructor takes a double and constructs a Decimal128 object given a roundMode, either + * to full precision, or with a fixed precision of 15 decimal digits. When a double is used to + * store a decimal floating point number, it is only correct up to 15 digits after converting + * back to decimal, so the 15 digit rounding is used for mixed-mode operations. * The general idea is to quantize the direct double->dec128 conversion * with a quantum of 1E(-15 +/- base10 exponent equivalent of the double). * To do this, we find the smallest (abs value) base 10 exponent greater * than the double's base 2 exp and shift the quantizer's exp accordingly. */ - Decimal128(double doubleValue, RoundingMode roundMode = kRoundTiesToEven); + explicit Decimal128(double doubleValue, + RoundingPrecision roundPrecision = kRoundTo15Digits, + RoundingMode roundMode = kRoundTiesToEven); /** * This constructor takes a string and constructs a Decimal128 object from it. @@ -152,7 +185,7 @@ public: * "200E9999999999" --> +Inf * "-200E9999999999" --> -Inf */ - Decimal128(std::string stringValue, RoundingMode roundMode = kRoundTiesToEven); + explicit Decimal128(std::string stringValue, RoundingMode roundMode = kRoundTiesToEven); /** * This function gets the inner Value struct storing a Decimal128 value. @@ -160,10 +193,50 @@ public: Value getValue() const; /** - * This function returns the decimal absolute value of the caller. + * Extracts the biased exponent from the combination field. + */ + uint32_t getBiasedExponent() const { + const uint64_t combo = _getCombinationField(); + if (combo < kCombinationNonCanonical) + return combo >> 3; + + return combo >= kCombinationInfinity + ? kMaxBiasedExponent + 1 // NaN or Inf + : (combo >> 1) & ((1 << 14) - 1); // non-canonical representation + } + + /** + * Returns the high 49 bits of the 113-bit binary encoded coefficient. Returns 0 for + * non-canonical or non-finite numbers. + */ + uint64_t getCoefficientHigh() const { + return _getCombinationField() < kCombinationNonCanonical + ? _value.high64 & kCanonicalCoefficientHighFieldMask + : 0; + } + + /** + * Returns the low 64 bits of the 113-bit binary encoded coefficient. Returns 0 for + * non-canonical or non-finite numbers. + */ + uint64_t getCoefficientLow() const { + return _getCombinationField() < kCombinationNonCanonical ? _value.low64 : 0; + } + + /** + * Returns the absolute value of this. */ Decimal128 toAbs() const; + /** + * Returns `this` with inverted sign bit + */ + Decimal128 negate() const { + Value negated = {_value.low64, _value.high64 ^ (1ULL << 63)}; + return Decimal128(negated); + } + + /** * This set of functions converts a Decimal128 to a certain integer type with a * given rounding mode. @@ -302,8 +375,31 @@ public: bool isLess(const Decimal128& other) const; bool isLessEqual(const Decimal128& other) const; + /** + * Returns true iff 'this' and 'other' are bitwise identical. Note that this returns false + * even for values that may convert to identical strings, such as different NaNs or + * non-canonical representations that represent bit-patterns never generated by any conforming + * implementation, but should be treated as 0. Mostly for testing. + */ + bool isBinaryEqual(const Decimal128& other) const { + return _value.high64 == other._value.high64 && _value.low64 == other._value.low64; + } + private: + static const uint8_t kSignFieldPos = 64 - 1; + static const uint8_t kCombinationFieldPos = kSignFieldPos - 17; + static const uint64_t kCombinationFieldMask = (1 << 17) - 1; + static const uint64_t kExponentFieldPos = kCombinationFieldPos + 3; + static const uint64_t kCoefficientContinuationFieldMask = (1ull << kCombinationFieldPos) - 1; + static const uint64_t kCombinationNonCanonical = 3 << 15; + static const uint64_t kCombinationInfinity = 0x1e << 12; + static const uint64_t kCombinationNaN = 0x1f << 12; + static const uint64_t kCanonicalCoefficientHighFieldMask = (1ull << 49) - 1; + + uint64_t _getCombinationField() const { + return (_value.high64 >> kCombinationFieldPos) & kCombinationFieldMask; + } + Value _value; }; - } // namespace mongo diff --git a/src/mongo/platform/decimal128_dummy.cpp b/src/mongo/platform/decimal128_dummy.cpp index d2db7d5892a..59a4a64e78b 100644 --- a/src/mongo/platform/decimal128_dummy.cpp +++ b/src/mongo/platform/decimal128_dummy.cpp @@ -41,7 +41,9 @@ Decimal128::Decimal128(int64_t int64Value) { invariant(false); } -Decimal128::Decimal128(double doubleValue, RoundingMode roundMode) { +Decimal128::Decimal128(double doubleValue, + RoundingPrecision roundPrecision, + RoundingMode roundMode) { invariant(false); } @@ -207,4 +209,6 @@ const Decimal128 Decimal128::kNegativeInfinity = Decimal128(); const Decimal128 Decimal128::kPositiveNaN = Decimal128(); const Decimal128 Decimal128::kNegativeNaN = Decimal128(); +const Decimal128 Decimal128::kNormalizedZero = {}; + } // namespace mongo diff --git a/src/mongo/platform/decimal128_test.cpp b/src/mongo/platform/decimal128_test.cpp index a90eca6ee7c..296d6490514 100644 --- a/src/mongo/platform/decimal128_test.cpp +++ b/src/mongo/platform/decimal128_test.cpp @@ -39,6 +39,11 @@ namespace mongo { // Tests for Decimal128 constructors +TEST(Decimal128Test, TestDefaultConstructor) { + Decimal128 d; + ASSERT_TRUE(d.isBinaryEqual(Decimal128(0))); +} + TEST(Decimal128Test, TestInt32ConstructorZero) { int32_t intZero = 0; Decimal128 d(intZero); @@ -185,13 +190,15 @@ TEST(Decimal128Test, TestDoubleConstructorNeg) { TEST(Decimal128Test, TestDoubleConstructorMaxRoundDown) { double doubleMax = DBL_MAX; - Decimal128 d(doubleMax, Decimal128::RoundingMode::kRoundTowardNegative); + Decimal128 d( + doubleMax, Decimal128::kRoundTo15Digits, Decimal128::RoundingMode::kRoundTowardNegative); ASSERT_EQUALS(d.toString(), "1.79769313486231E+308"); } TEST(Decimal128Test, TestDoubleConstructorMaxRoundUp) { double doubleMax = DBL_MAX; - Decimal128 d(doubleMax, Decimal128::RoundingMode::kRoundTowardPositive); + Decimal128 d( + doubleMax, Decimal128::kRoundTo15Digits, Decimal128::RoundingMode::kRoundTowardPositive); ASSERT_EQUALS(d.toString(), "1.79769313486232E+308"); } @@ -269,6 +276,39 @@ TEST(Decimal128Test, TestStringConstructorNaN) { ASSERT_EQUALS(val.low64, lowBytes); } +TEST(Decimal128Test, TestNonCanonicalDecimal) { + // It is possible to encode a significand with more than 34 decimal digits. + // Conforming implementations should not generate these, but they must be treated as zero + // when encountered. However, the exponent and sign still matter. + + // 0x6c10000000000000 0000000000000000 = non-canonical 0, all ignored bits clear + Decimal128 nonCanonical0E0(Decimal128::Value{0, 0x6c10000000000000ull}); + std::string zeroE0 = nonCanonical0E0.toString(); + ASSERT_EQUALS(zeroE0, "0"); + + // 0xec100000deadbeef 0123456789abcdef = non-canonical -0, random stuff in ignored bits + Decimal128 nonCanonicalM0E0(Decimal128::Value{0x0123456789abcdefull, 0xec100000deadbeefull}); + std::string minusZeroE0 = nonCanonicalM0E0.toString(); + ASSERT_EQUALS(minusZeroE0, "-0"); + + // 0x6c11fffffffffffff ffffffffffffffff = non-canonical 0.000, all ignored bits set + Decimal128 nonCanonical0E3(Decimal128::Value{0xffffffffffffffffull, 0x6c11ffffffffffffull}); + std::string zeroE3 = nonCanonical0E3.toString(); + ASSERT_EQUALS(zeroE3, "0E+3"); + + // Check extraction functions, they should treat this as the corresponding zero as well. + ASSERT_EQUALS(nonCanonical0E3.getBiasedExponent(), Decimal128("0E+3").getBiasedExponent()); + ASSERT_EQUALS(nonCanonical0E3.getCoefficientHigh(), 0u); + ASSERT_EQUALS(nonCanonical0E3.getCoefficientLow(), 0u); + + // Check doing some arithmetic opations and number conversions + const double minusZeroDouble = nonCanonicalM0E0.toDouble(); + ASSERT_EQUALS(minusZeroDouble, 0.0); + ASSERT_EQUALS(-1.0, std::copysign(1.0, minusZeroDouble)); + ASSERT_TRUE(nonCanonical0E3.add(Decimal128(1)).isEqual(Decimal128(1))); + ASSERT_TRUE(Decimal128(1).divide(nonCanonicalM0E0).isEqual(Decimal128::kNegativeInfinity)); +} + // Tests for absolute value function TEST(Decimal128Test, TestAbsValuePos) { Decimal128 d(25); diff --git a/src/mongo/scripting/mozjs/valuewriter.cpp b/src/mongo/scripting/mozjs/valuewriter.cpp index 8b811f1afc1..44465ef86d5 100644 --- a/src/mongo/scripting/mozjs/valuewriter.cpp +++ b/src/mongo/scripting/mozjs/valuewriter.cpp @@ -33,6 +33,7 @@ #include #include "mongo/base/error_codes.h" +#include "mongo/platform/decimal128.h" #include "mongo/scripting/mozjs/exception.h" #include "mongo/scripting/mozjs/implscope.h" #include "mongo/scripting/mozjs/jsstringwrapper.h" @@ -164,7 +165,7 @@ int64_t ValueWriter::toInt64() { Decimal128 ValueWriter::toDecimal128() { if (_value.isNumber()) { - return Decimal128(toNumber()); + return Decimal128(toNumber(), Decimal128::kRoundTo15Digits); } if (getScope(_context)->getProto().instanceOf(_value)) diff --git a/src/mongo/util/safe_num.cpp b/src/mongo/util/safe_num.cpp index cf8af43377c..61c0d4856a4 100644 --- a/src/mongo/util/safe_num.cpp +++ b/src/mongo/util/safe_num.cpp @@ -150,7 +150,7 @@ bool SafeNum::isIdentical(const SafeNum& rhs) const { case NumberDouble: return _value.doubleVal == rhs._value.doubleVal; case NumberDecimal: - return Decimal128(_value.decimalVal).isEqual(rhs._value.decimalVal); + return Decimal128(_value.decimalVal).isEqual(Decimal128(rhs._value.decimalVal)); case EOO: // EOO doesn't match anything, including itself. default: @@ -187,15 +187,15 @@ double SafeNum::getDouble(const SafeNum& snum) { Decimal128 SafeNum::getDecimal(const SafeNum& snum) { switch (snum._type) { case NumberInt: - return snum._value.int32Val; + return Decimal128(snum._value.int32Val); case NumberLong: - return snum._value.int64Val; + return Decimal128(snum._value.int64Val); case NumberDouble: - return snum._value.doubleVal; + return Decimal128(snum._value.doubleVal, Decimal128::kRoundTo15Digits); case NumberDecimal: - return snum._value.decimalVal; + return Decimal128(snum._value.decimalVal); default: - return 0.0; + return Decimal128::kNormalizedZero; } } -- cgit v1.2.1