Files
FESADev/tests/unit/math/high_low_arithmetic_test.cpp
T

223 lines
9.3 KiB
C++

#include "../../../src/fesa/math/high_low_arithmetic.h"
#include <gtest/gtest.h>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <limits>
#include <random>
#include <vector>
#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<double>::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<double>::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<double>::max()},
HighLow{std::numeric_limits<double>::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<double>::denorm_min());
EXPECT_FALSE(IsFinite(TwoProduct(std::nextafter(1.0, 2.0),
std::nextafter(std::numeric_limits<double>::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;
}
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<double> values{0.0, -0.0,
std::numeric_limits<double>::denorm_min(),
-std::numeric_limits<double>::denorm_min(),
std::numeric_limits<double>::min(),
std::nextafter(std::numeric_limits<double>::min(), 0.0),
std::numeric_limits<double>::max(),
-std::numeric_limits<double>::max(), 1.0, -1.0,
std::nextafter(1.0, 2.0), std::ldexp(1.0, -53),
std::ldexp(1.0, -54), std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::infinity(),
std::numeric_limits<double>::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<double>::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