#include "../../../src/fesa/math/high_low_arithmetic.h" #include #include #include #include #include #include #include #include "high_low_arithmetic_original.h" namespace { using namespace fesa::math_internal; TEST(HighLowArithmetic, RetainsSubUlpAdditionsAndSignedCancellation) { ASSERT_TRUE(ArithmeticEnvironmentSupported()); EXPECT_FALSE(IsNormalized(HighLow{-0.0, -0.0})); EXPECT_TRUE(IsNormalized(HighLow{-0.0})); EXPECT_FALSE(std::signbit(HighLow{-0.0}.high)); EXPECT_FALSE(std::signbit(Negate(HighLow{}).high)); EXPECT_FALSE(std::signbit(Negate(HighLow{}).low)); const double tiny = std::ldexp(1.0, -60); const auto positive = Add(HighLow{1}, HighLow{tiny}); EXPECT_EQ(positive.high, 1); EXPECT_EQ(positive.low, tiny); EXPECT_EQ(Rounded(positive), 1); const auto difference = Subtract(positive, HighLow{1}); EXPECT_EQ(difference.high, tiny); EXPECT_EQ(difference.low, 0); EXPECT_EQ(Compare(positive, HighLow{1}), 1); EXPECT_EQ(Compare(HighLow{1, -tiny}, HighLow{1}), -1); EXPECT_EQ(Compare(Add(positive, Negate(positive)), HighLow{}), 0); EXPECT_EQ(Abs(HighLow{-1, -tiny}).low, tiny); } TEST(HighLowArithmetic, AllProductTermsAndDivisionResidualReachState) { const double tiny = std::ldexp(1.0, -60); const auto product = Multiply(HighLow{1, tiny}, HighLow{1, -tiny}); EXPECT_EQ(product.high, 1); EXPECT_EQ(product.low, -std::ldexp(1.0, -120)); const auto quotient = Divide(HighLow{1}, HighLow{3}); ASSERT_TRUE(IsNormalized(quotient)); const auto defect = Subtract(Multiply(quotient, HighLow{3}), HighLow{1}); EXPECT_LE(std::abs(defect.high), std::ldexp(1.0, -104)); const auto cancellation = Subtract(HighLow{1, tiny}, HighLow{1, tiny / 2}); EXPECT_EQ(cancellation.high, tiny / 2); EXPECT_EQ(cancellation.low, 0); } TEST(HighLowArithmetic, NormalizesTiesAndCrossesPowerOfTwoBoundary) { const double half_ulp = std::ldexp(1.0, -53); EXPECT_TRUE(IsNormalized(HighLow{1, half_ulp})); EXPECT_FALSE(IsNormalized(HighLow{std::nextafter(1.0, 2.0), -half_ulp})); auto result = HighLow{1}; for (int i = 0; i < 4; ++i) result = Add(result, HighLow{half_ulp / 2}); EXPECT_EQ(result.high, std::nextafter(1.0, 2.0)); EXPECT_EQ(result.low, 0); const auto minimum = HighLow{std::numeric_limits::denorm_min()}; EXPECT_EQ(Add(minimum, minimum).high, 2 * minimum.high); EXPECT_EQ(Multiply(minimum, HighLow{1}).high, minimum.high); } TEST(HighLowArithmetic, RejectsUnsupportedDyadicTailAndNonfiniteResults) { const auto minimum = HighLow{std::numeric_limits::denorm_min()}; EXPECT_FALSE(IsFinite(Multiply(minimum, HighLow{0.5}))); EXPECT_FALSE(IsFinite(Multiply(HighLow{std::ldexp(1.0, -1022)}, HighLow{std::nextafter(0.5, 1.0)}))); EXPECT_FALSE(IsFinite(Divide(minimum, HighLow{2}))); EXPECT_FALSE(IsFinite(Divide(HighLow{1}, HighLow{}))); EXPECT_FALSE(IsFinite(Add(HighLow{std::numeric_limits::max()}, HighLow{std::numeric_limits::max()}))); EXPECT_EQ(Compare(InvalidHighLow(), HighLow{}), 2); EXPECT_FALSE(IsNormalized(HighLow{1, 1})); EXPECT_TRUE(std::isnan(Rounded(HighLow{1, 1}))); } TEST(HighLowArithmetic, ExactDyadicOracleBoundsDiscardedTailAndRefinement) { // Expected pairs were obtained independently using exact rational arithmetic. // Add and multiply deliberately discard a term at 2^-108; neither is exact. for (int scale : {-100, 0, 100}) { const auto x = HighLow{std::ldexp(1.0, scale), std::ldexp(1.0, scale-54)}; const auto y = HighLow{std::ldexp(1.0, scale-53), std::ldexp(1.0, scale-108)}; const auto sum = Add(x, y); EXPECT_EQ(sum.high, std::ldexp(0x1.0000000000001p+0, scale)); EXPECT_EQ(sum.low, -std::ldexp(1.0, scale-54)); const auto square = Multiply(x, x); EXPECT_EQ(square.high, std::ldexp(1.0, 2*scale)); EXPECT_EQ(square.low, std::ldexp(1.0, 2*scale-53)); } const auto third = Divide(HighLow{1}, HighLow{3}); EXPECT_EQ(third.high, 0x1.5555555555555p-2); EXPECT_EQ(third.low, 0x1.5555555555555p-56); const auto third_residual = Subtract(Multiply(HighLow{3}, third), HighLow{1}); EXPECT_EQ(third_residual.high, -std::ldexp(1.0, -108)); EXPECT_EQ(third_residual.low, 0); const auto refined = Divide(HighLow{1, std::ldexp(1.0,-60)}, HighLow{1.25, -std::ldexp(1.0,-60)}); EXPECT_EQ(refined.high, 0x1.999999999999ap-1); EXPECT_EQ(refined.low, -0x1.8e147ae147ae1p-55); const auto boundary = TwoProduct(0x1.0000000000001p+0, 0x1.0000000000001p-970); EXPECT_EQ(boundary.high, 0x1.0000000000002p-970); EXPECT_EQ(boundary.low, std::numeric_limits::denorm_min()); EXPECT_FALSE(IsFinite(TwoProduct(std::nextafter(1.0, 2.0), std::nextafter(std::numeric_limits::min(), 1.0)))); } TEST(HighLowArithmetic, RejectsChangedRoundingAndFlushToZeroEnvironment) { struct Restore { int rounding = std::fegetround(); #if defined(_M_X64) || defined(__SSE2__) unsigned csr = _mm_getcsr(); #endif ~Restore() { std::fesetround(rounding); #if defined(_M_X64) || defined(__SSE2__) _mm_setcsr(csr); #endif } } restore; ASSERT_TRUE(ArithmeticEnvironmentSupported()); ASSERT_EQ(std::fesetround(FE_DOWNWARD), 0); EXPECT_FALSE(ArithmeticEnvironmentSupported()); ASSERT_EQ(std::fesetround(FE_TONEAREST), 0); #if defined(_M_X64) || defined(__SSE2__) _mm_setcsr(restore.csr | 0x8000U); EXPECT_FALSE(ArithmeticEnvironmentSupported()); _mm_setcsr(restore.csr | 0x0040U); EXPECT_FALSE(ArithmeticEnvironmentSupported()); #endif } std::uint64_t Bits(const double value) { std::uint64_t result = 0; std::memcpy(&result, &value, sizeof(result)); return result; } double FromBits(const std::uint64_t bits) { double result = 0; std::memcpy(&result, &bits, sizeof(result)); return result; } TEST(HighLowArithmetic, FiniteClassificationMatchesBinary64Boundaries) { const auto check = [](const double value) { const bool expected = std::isfinite(value); EXPECT_EQ(IsFinite(HighLow{value, 0.0}), expected); EXPECT_EQ(IsFinite(HighLow{1.0, value}), expected); }; for (std::uint64_t exponent = 0; exponent < 2048U; ++exponent) { for (const auto fraction : {0ULL, 1ULL, 1ULL << 51U, (1ULL << 52U) - 1ULL}) { for (const auto sign : {0ULL, 1ULL << 63U}) check(FromBits(sign | (exponent << 52U) | fraction)); } } std::mt19937_64 random{0x5800f17U}; for (int sample = 0; sample < 10000; ++sample) check(FromBits(random())); } void ExpectSamePair(const HighLow actual, const fesa::high_low_original::HighLow expected) { EXPECT_EQ(Bits(actual.high), Bits(expected.high)); EXPECT_EQ(Bits(actual.low), Bits(expected.low)); } TEST(HighLowArithmetic, BitExponentMatchesFrozenOracleAcrossEveryExponent) { ASSERT_TRUE(ArithmeticEnvironmentSupported()); // Every normal exponent, every possible trailing significand-bit count, // both signs; then every subnormal single-bit and mixed-bit boundary. for (std::uint64_t exponent = 1; exponent < 2047; ++exponent) { for (unsigned trailing = 0; trailing <= 52U; ++trailing) { const auto fraction = trailing == 52U ? 0ULL : 1ULL << trailing; for (const auto sign : {0ULL, 1ULL << 63U}) { const double value = FromBits(sign | (exponent << 52U) | fraction); ASSERT_EQ(LowestBitExponent(value), fesa::high_low_original::LowestBitExponent(value)); } } } for (unsigned bit = 0; bit < 52U; ++bit) { for (const auto significand : {1ULL << bit, (1ULL << 52U) - (1ULL << bit)}) { const double value = FromBits(significand); EXPECT_EQ(LowestBitExponent(value), fesa::high_low_original::LowestBitExponent(value)); } } } TEST(HighLowArithmetic, OptimizedArithmeticMatchesFrozenOriginalBits) { ASSERT_TRUE(ArithmeticEnvironmentSupported()); std::vector values{0.0, -0.0, std::numeric_limits::denorm_min(), -std::numeric_limits::denorm_min(), std::numeric_limits::min(), std::nextafter(std::numeric_limits::min(), 0.0), std::numeric_limits::max(), -std::numeric_limits::max(), 1.0, -1.0, std::nextafter(1.0, 2.0), std::ldexp(1.0, -53), std::ldexp(1.0, -54), std::numeric_limits::infinity(), -std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}; std::mt19937_64 random{0x58a0fU}; for (int sample = 0; sample < 4000; ++sample) { values.push_back(FromBits(random())); } for (std::size_t index = 0; index < values.size(); ++index) { const double high = values[index]; const double low = values[(index * 13U + 7U) % values.size()]; const fesa::high_low_original::HighLow original{high, low}; EXPECT_EQ(IsNormalized({high, low}), fesa::high_low_original::IsNormalized(original)); // Normalize a raw pair with the frozen operation, never the new predicate. const auto a = fesa::high_low_original::TwoSum(high, low); const auto b = fesa::high_low_original::TwoSum( values[(index + 1U) % values.size()], values[(index * 3U + 2U) % values.size()]); ExpectSamePair(Add({a.high, a.low}, {b.high, b.low}), fesa::high_low_original::Add(a, b)); ExpectSamePair(Multiply({a.high, a.low}, {b.high, b.low}), fesa::high_low_original::Multiply(a, b)); ExpectSamePair(Divide({a.high, a.low}, {b.high, b.low}), fesa::high_low_original::Divide(a, b)); ExpectSamePair(TwoProduct(high, low), fesa::high_low_original::TwoProduct(high, low)); } for (double high : {1.0, std::nextafter(1.0, 2.0), -1.0, std::numeric_limits::max()}) { for (double low : {0.0, -0.0, std::ldexp(1.0, -53), -std::ldexp(1.0, -53)}) { EXPECT_EQ(IsNormalized({high, low}), fesa::high_low_original::IsNormalized({high, low})); } } } } // namespace