This commit is contained in:
@@ -12,31 +12,18 @@ public static partial class DDMath
|
||||
return DoubleDouble.Abs(value);
|
||||
}
|
||||
|
||||
/// <summary>Returns the reciprocal with double-double precision.</summary>
|
||||
/// <remarks>
|
||||
/// Specializes scalar/DD division for a numerator of one, omitting only its
|
||||
/// redundant numerator checks. The arithmetic sequence and boundary handling
|
||||
/// are unchanged, preserving the high and low component bits of <c>1.0 / value</c>.
|
||||
/// Results are approximate, not guaranteed correctly rounded;
|
||||
/// finite results are tested against a relative error bound of 2^-100 plus one
|
||||
/// minimum binary64 subnormal. Precision decreases near underflow.
|
||||
/// Signed zeros map to signed infinities, signed infinities to signed zeros,
|
||||
/// and NaN to canonical NaN. Finite overflow produces signed infinity.
|
||||
/// </remarks>
|
||||
/// <inheritdoc cref="DoubleDouble.Square"/>
|
||||
[Pure]
|
||||
public static DoubleDouble Square(DoubleDouble value)
|
||||
{
|
||||
return DoubleDouble.Square(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="DoubleDouble.Reciprocal"/>
|
||||
[Pure]
|
||||
public static DoubleDouble Reciprocal(DoubleDouble value)
|
||||
{
|
||||
const double one = 1.0;
|
||||
if (!DoubleDouble.IsFinite(value) || value._high == 0.0)
|
||||
{
|
||||
return new DoubleDouble(one / value._high);
|
||||
}
|
||||
if (!PreciseMathHelper.IsDivisionWithinFastRange(value._high))
|
||||
{
|
||||
return PreciseMathHelper.DivideBoundary(one, value);
|
||||
}
|
||||
|
||||
return PreciseMathHelper.DivideScalarFinite(one, value);
|
||||
return DoubleDouble.Reciprocal(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="DoubleDouble.Cbrt"/>
|
||||
|
||||
@@ -6,6 +6,43 @@ public readonly partial struct DoubleDouble :
|
||||
IMultiplyOperators<DoubleDouble, double, DoubleDouble>,
|
||||
IDivisionOperators<DoubleDouble, double, DoubleDouble>
|
||||
{
|
||||
/// <summary>Returns the square using a specialized double-double product.</summary>
|
||||
/// <remarks>
|
||||
/// Combines the equal cross terms before FMA accumulation, retaining low squared.
|
||||
/// Results are approximate and may differ in low bits from value * value;
|
||||
/// finite results are tested against 2^-100 relative error plus one minimum
|
||||
/// binary64 subnormal, not a guarantee of correctly rounded components.
|
||||
/// Either zero maps to positive zero, either infinity to positive infinity,
|
||||
/// and NaN to canonical NaN. Uses the allocating multiplication fallback at
|
||||
/// exponent boundaries. No performance improvement is guaranteed.
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
public static DoubleDouble Square(DoubleDouble value)
|
||||
{
|
||||
if (!IsFinite(value) || value._high == 0.0)
|
||||
{
|
||||
return new DoubleDouble(value._high * value._high);
|
||||
}
|
||||
if (!PreciseMathHelper.IsMultiplicationWithinFastRange(value._high, value._high))
|
||||
{
|
||||
return PreciseMathHelper.MultiplyBoundary(value, value);
|
||||
}
|
||||
|
||||
(double product, double error) = PreciseMathHelper.TwoSquare(value._high);
|
||||
// The guard bounds the high exponent to [-450, 450], so doubling high
|
||||
// is exact and finite. FMA avoids rounding the cross product separately.
|
||||
error = Math.FusedMultiplyAdd(value._high + value._high, value._low, error);
|
||||
// As for multiplication, normalized input makes |error| < 4u*product;
|
||||
// the positive high product is normal and the corrected sum is finite.
|
||||
(double high, double low) = PreciseMathHelper.TwoQuickAdd(product, error);
|
||||
// Incorporate low squared AFTER the cross term has been absorbed into
|
||||
// high. Otherwise an exactly representable tail can round away before
|
||||
// cancellation, e.g. (1 - 2^-54)^2 loses its 2^-108 residual.
|
||||
low = Math.FusedMultiplyAdd(value._low, value._low, low);
|
||||
(high, low) = PreciseMathHelper.TwoQuickAdd(high, low);
|
||||
return new DoubleDouble(high, low == 0.0 ? 0.0 : low);
|
||||
}
|
||||
|
||||
/// <summary>Returns the operand unchanged.</summary>
|
||||
public static DoubleDouble operator +(DoubleDouble value)
|
||||
{
|
||||
|
||||
@@ -22,6 +22,37 @@ public readonly partial struct DoubleDouble : IFormattable
|
||||
return ToString(null, provider);
|
||||
}
|
||||
|
||||
/// <summary>Returns the exact decimal expansion of the stored value using invariant culture.</summary>
|
||||
/// <remarks>
|
||||
/// Uses allocating integer arithmetic, without rounding or scientific notation.
|
||||
/// Omits unnecessary fractional zeros and preserves negative zero; nonfinite
|
||||
/// values use NaN, Infinity, and -Infinity. Sparse pairs can require over a
|
||||
/// thousand characters. Parse with invariant culture to recover the components;
|
||||
/// this is an exact-value format, not a shortest-round-trip format.
|
||||
/// </remarks>
|
||||
public string ToStringExact()
|
||||
{
|
||||
if (!double.IsFinite(_high) || _high == 0.0)
|
||||
{
|
||||
return _high.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
(BigInteger numerator, BigInteger denominator) = ConversionFraction();
|
||||
// A sum of binary64 values has a power-of-two denominator. Multiplying
|
||||
// by the matching power of five makes an integer coefficient over 10^n.
|
||||
int decimalPlaces = (int)(denominator.GetBitLength() - 1);
|
||||
BigInteger coefficient = BigInteger.Abs(numerator) * BigInteger.Pow(5, decimalPlaces);
|
||||
string digits = coefficient.ToString(CultureInfo.InvariantCulture);
|
||||
int end = digits.Length;
|
||||
while (decimalPlaces > 0 && digits[end - 1] == '0')
|
||||
{
|
||||
end--;
|
||||
decimalPlaces--;
|
||||
}
|
||||
string sign = numerator.Sign < 0 ? "-" : string.Empty;
|
||||
return sign + FormattingFixed(digits[..end], decimalPlaces, ".");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the exact component sum, rounding to nearest with ties to even.
|
||||
/// Supports G/g (significant digits, default 32; G0 also means 32), E/e
|
||||
|
||||
@@ -5,6 +5,33 @@ public readonly partial struct DoubleDouble
|
||||
/// <summary>Gets the binary radix of the components.</summary>
|
||||
public static int Radix => 2;
|
||||
|
||||
/// <summary>Returns the reciprocal with double-double precision.</summary>
|
||||
/// <remarks>
|
||||
/// Specializes scalar/DD division for a numerator of one, omitting only its
|
||||
/// redundant numerator checks. The arithmetic sequence and boundary handling
|
||||
/// are unchanged, preserving the high and low component bits of <c>1.0 / value</c>.
|
||||
/// Results are approximate, not guaranteed correctly rounded;
|
||||
/// finite results are tested against a relative error bound of 2^-100 plus one
|
||||
/// minimum binary64 subnormal. Precision decreases near underflow.
|
||||
/// Signed zeros map to signed infinities, signed infinities to signed zeros,
|
||||
/// and NaN to canonical NaN. Finite overflow produces signed infinity.
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
public static DoubleDouble Reciprocal(DoubleDouble value)
|
||||
{
|
||||
const double one = 1.0;
|
||||
if (!DoubleDouble.IsFinite(value) || value._high == 0.0)
|
||||
{
|
||||
return new DoubleDouble(one / value._high);
|
||||
}
|
||||
if (!PreciseMathHelper.IsDivisionWithinFastRange(value._high))
|
||||
{
|
||||
return PreciseMathHelper.DivideBoundary(one, value);
|
||||
}
|
||||
|
||||
return PreciseMathHelper.DivideScalarFinite(one, value);
|
||||
}
|
||||
|
||||
/// <summary>Returns the absolute value, preserving both components and canonicalizing NaN.</summary>
|
||||
public static DoubleDouble Abs(DoubleDouble value)
|
||||
{
|
||||
|
||||
@@ -244,7 +244,8 @@ public readonly partial struct DoubleDouble : IRootFunctions<DoubleDouble>
|
||||
|
||||
/// <summary>Returns the nonnegative square root with double-double precision.</summary>
|
||||
/// <remarks>
|
||||
/// Uses power-of-two scaling and an FMA-based Newton correction. Results are
|
||||
/// Uses power-of-two scaling and an FMA-based Newton correction. An exponent-
|
||||
/// tracked correction retains sparse lows endangered by downscaling. Results are
|
||||
/// approximate, not guaranteed correctly rounded; finite positive inputs are
|
||||
/// tested against a relative error bound of 2^-100 across the binary64 range.
|
||||
/// Signed zero and positive infinity are preserved. Negative nonzero values
|
||||
@@ -264,11 +265,19 @@ public readonly partial struct DoubleDouble : IRootFunctions<DoubleDouble>
|
||||
|
||||
// Choose an even exponent, rounding negative odd exponents down too.
|
||||
// For positive finite normalized inputs, exponent is in [-1074, 1022]
|
||||
// and the scaled high is in [1, 4). Scaling the high is exact; any
|
||||
// underflow in a very sparse low is far below the relative error bound.
|
||||
// and the scaled high is in [1, 4). Scaling the high is exact.
|
||||
int exponent = Math.ILogB(value.High) & ~1;
|
||||
double high = Math.ScaleB(value.High, -exponent);
|
||||
double low = Math.ScaleB(value.Low, -exponent);
|
||||
// Dividing the scaled low by 2*estimate (in [2, 4]) can round at the
|
||||
// subnormal floor before output rescaling restores its magnitude. In
|
||||
// that sparse domain, refine high alone and apply the original low with
|
||||
// separate exponents afterward. Never add both the scaled and original low.
|
||||
bool separateLow = exponent > 0 && value.Low != 0.0 && Math.Abs(low) < Math.ScaleB(1.0, -1020);
|
||||
if (separateLow)
|
||||
{
|
||||
low = 0.0;
|
||||
}
|
||||
double estimate = Math.Sqrt(high);
|
||||
|
||||
// Estimate is in [1, 2]. FMA avoids rounding its square before
|
||||
@@ -282,7 +291,10 @@ public readonly partial struct DoubleDouble : IRootFunctions<DoubleDouble>
|
||||
// Normalize again to canonicalize a zero low, including any underflow
|
||||
// of an exceptionally sparse correction during rescaling.
|
||||
int rootExponent = exponent / 2;
|
||||
return DoubleDouble.FromComponents(Math.ScaleB(root.High, rootExponent), Math.ScaleB(root.Low, rootExponent));
|
||||
root = DoubleDouble.FromComponents(Math.ScaleB(root.High, rootExponent), Math.ScaleB(root.Low, rootExponent));
|
||||
// In this branch |originalLow/originalHigh| < 2^-1020. The omitted
|
||||
// quadratic term is negligible even relative to the low correction.
|
||||
return separateLow ? root + RootLowCorrection(value, root, 2) : root;
|
||||
}
|
||||
|
||||
/// <summary>Returns the reciprocal square root with double-double precision.</summary>
|
||||
|
||||
@@ -978,6 +978,114 @@ public class DoubleDoubleArithmeticTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-450)]
|
||||
[InlineData(0)]
|
||||
[InlineData(450)]
|
||||
public void SquareRetainsExactCrossAndLowSquaredTerms(int exponent)
|
||||
{
|
||||
// (h - h*2^-54)^2 = h^2 - h^2*2^-53 + h^2*2^-108.
|
||||
// (h + h*2^-53)^2 = h^2 + h^2*2^-52 + h^2*2^-106.
|
||||
// Both sums fit exactly in two components, including fast-range endpoints.
|
||||
double high = Math.ScaleB(1.0, exponent);
|
||||
double highSquared = Math.ScaleB(1.0, 2 * exponent);
|
||||
DoubleDouble below = DoubleDouble.FromComponents(high, -Math.ScaleB(1.0, exponent - 54));
|
||||
DoubleDouble above = DoubleDouble.FromComponents(high, Math.ScaleB(1.0, exponent - 53));
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
CheckBoundary(DoubleDouble.Square(sign * below), Math.BitDecrement(highSquared), Math.ScaleB(1.0, (2 * exponent) - 108));
|
||||
CheckBoundary(DDMath.Square(sign * above), Math.BitIncrement(highSquared), Math.ScaleB(1.0, (2 * exponent) - 106));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareHandlesSpecialValuesAndExactBinaryPowers()
|
||||
{
|
||||
foreach (double value in new[] { 0.0, -0.0, double.NaN, double.PositiveInfinity, double.NegativeInfinity })
|
||||
{
|
||||
CheckBits(DoubleDouble.Square(new DoubleDouble(value)), value * value);
|
||||
CheckBits(DDMath.Square(new DoubleDouble(value)), value * value);
|
||||
}
|
||||
for (int exponent = -1074; exponent <= 1023; exponent++)
|
||||
{
|
||||
// A binary power squared needs only exponent arithmetic, not a DD oracle.
|
||||
double high = Math.ScaleB(1.0, exponent);
|
||||
double expected = Math.ScaleB(1.0, 2 * exponent);
|
||||
CheckBits(DoubleDouble.Square(new DoubleDouble(high)), expected);
|
||||
CheckBits(DoubleDouble.Square(new DoubleDouble(-high)), expected);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareMeetsExactRationalBoundsAcrossRangesAndDispatchTransitions()
|
||||
{
|
||||
Random random = new(57721);
|
||||
for (int exponent = -1074; exponent <= 1023; exponent++)
|
||||
{
|
||||
double high = Math.ScaleB(1.0 + random.NextDouble(), exponent);
|
||||
double low = Math.ScaleB(random.NextDouble(), exponent - 53);
|
||||
foreach (double residual in new[] { 0.0, low, -low, double.Epsilon, -double.Epsilon })
|
||||
{
|
||||
AssertSquare(DoubleDouble.FromComponents(high, residual));
|
||||
}
|
||||
double power = Math.ScaleB(1.0, exponent);
|
||||
AssertSquare(new DoubleDouble(Math.BitDecrement(power)));
|
||||
AssertSquare(new DoubleDouble(power));
|
||||
AssertSquare(new DoubleDouble(Math.BitIncrement(power)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareUsesTheCompleteInputForOverflowAndUnderflowClassification()
|
||||
{
|
||||
// Neighborhoods of sqrt(overflow midpoint) and sqrt(epsilon/2).
|
||||
// The exact integer oracle, not Math.Sqrt or another DD operation,
|
||||
// decides which side of each threshold the actual stored input lies on.
|
||||
double overflowHigh = Math.ScaleB(1.0, 512);
|
||||
// At low=-2^457, the square exceeds the midpoint by low^2;
|
||||
// the next more-negative low puts it strictly below the midpoint.
|
||||
double overflowLow = -Math.ScaleB(1.0, 457);
|
||||
double underflowHigh = Math.ScaleB(Math.Sqrt(0.5), -537);
|
||||
double underflowLow = Math.ScaleB(1.0, -591);
|
||||
foreach (double high in new[] { Math.BitDecrement(overflowHigh), overflowHigh, Math.BitIncrement(overflowHigh) })
|
||||
{
|
||||
foreach (double low in new[] { Math.BitDecrement(overflowLow), overflowLow, Math.BitIncrement(overflowLow), 0.0 })
|
||||
{
|
||||
AssertSquare(DoubleDouble.FromComponents(high, low));
|
||||
}
|
||||
}
|
||||
foreach (double high in new[] { Math.BitDecrement(underflowHigh), underflowHigh, Math.BitIncrement(underflowHigh) })
|
||||
{
|
||||
foreach (double low in new[] { -underflowLow, 0.0, underflowLow })
|
||||
{
|
||||
AssertSquare(DoubleDouble.FromComponents(high, low));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertSquare(DoubleDouble value)
|
||||
{
|
||||
BigInteger input = Units(value);
|
||||
BigInteger numerator = input * input;
|
||||
BigInteger denominator = BigInteger.One << 1074;
|
||||
BigInteger overflow = Units(double.MaxValue) + Units(Math.ScaleB(1.0, 970));
|
||||
DoubleDouble actual = DoubleDouble.Square(value);
|
||||
DoubleDouble.IsCanonical(actual).ShouldBeTrue();
|
||||
DoubleDouble.IsNegative(actual).ShouldBeFalse();
|
||||
if (numerator >= overflow * denominator)
|
||||
{
|
||||
CheckBits(actual, double.PositiveInfinity);
|
||||
}
|
||||
else
|
||||
{
|
||||
AssertRelative(actual, numerator, denominator);
|
||||
// The absolute error floor must not hide a wrong zero/nonzero result.
|
||||
DoubleDouble.IsZero(actual).ShouldBe((numerator << 1) <= denominator);
|
||||
}
|
||||
CheckBoundary(DoubleDouble.Square(-value), actual.High, actual.Low);
|
||||
CheckBoundary(DDMath.Square(value), actual.High, actual.Low);
|
||||
}
|
||||
|
||||
private static (double[] Scalars, DoubleDouble[] Values) ScalarArithmeticCompatibilityCases()
|
||||
{
|
||||
List<double> scalars = [0.0, -0.0, 1.0, -1.0, 2.0, -2.0, 3.0, -3.0, 100.0, -100.0,
|
||||
|
||||
@@ -4,6 +4,79 @@ namespace Just.PreciseMath.Tests;
|
||||
|
||||
public class DoubleDoubleFormattingTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(1.25, 0.0, "1.25")]
|
||||
[InlineData(-1.25, 0.0, "-1.25")]
|
||||
[InlineData(1000.0, 0.0, "1000")]
|
||||
[InlineData(1.0, 5.551115123125783e-17, "1.000000000000000055511151231257827021181583404541015625")]
|
||||
[InlineData(9007199254740992.0, 1.0, "9007199254740993")]
|
||||
public void ExactFormattingPreservesTheCompleteDyadicValue(double high, double low, string expected)
|
||||
{
|
||||
// Exact binary fractions: the nonzero fractional low is 2^-54.
|
||||
DoubleDouble value = DoubleDouble.FromComponents(high, low);
|
||||
value.ToStringExact().ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExactFormattingUsesInvariantSpecialSymbolsAndPreservesZeroSigns()
|
||||
{
|
||||
CultureInfo original = CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");
|
||||
new DoubleDouble(-1.25).ToStringExact().ShouldBe("-1.25");
|
||||
DoubleDouble[] values = [DoubleDouble.Zero, DoubleDouble.NegativeZero,
|
||||
DoubleDouble.NaN, DoubleDouble.PositiveInfinity, DoubleDouble.NegativeInfinity];
|
||||
string[] expected = ["0", "-0", "NaN", "Infinity", "-Infinity"];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
string text = values[i].ToStringExact();
|
||||
text.ShouldBe(expected[i]);
|
||||
DoubleDouble parsed = DoubleDouble.Parse(text, CultureInfo.InvariantCulture);
|
||||
BitConverter.DoubleToInt64Bits(parsed.High).ShouldBe(BitConverter.DoubleToInt64Bits(values[i].High));
|
||||
BitConverter.DoubleToInt64Bits(parsed.Low).ShouldBe(0L);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = original;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExactFormattingPreservesSparsePairsBeyondTheStandardPrecisionLimit()
|
||||
{
|
||||
DoubleDouble[] values = [new(double.Epsilon), new(double.MaxValue), DoubleDouble.Pi,
|
||||
DoubleDouble.FromComponents(1.0, double.Epsilon),
|
||||
DoubleDouble.FromComponents(1.0, -double.Epsilon),
|
||||
DoubleDouble.FromComponents(double.MaxValue, double.Epsilon),
|
||||
DoubleDouble.FromComponents(double.MaxValue, -double.Epsilon),
|
||||
DoubleDouble.FromComponents(double.MaxValue, Math.BitDecrement(Math.ScaleB(1.0, 970)))];
|
||||
foreach (DoubleDouble value in values)
|
||||
{
|
||||
AssertExactText(value);
|
||||
AssertExactText(-value);
|
||||
}
|
||||
DoubleDouble.FromComponents(1.0, double.Epsilon).ToStringExact().Length.ShouldBeGreaterThan(999);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExactFormattingMatchesIndependentIntegerValuesAcrossEveryExponent()
|
||||
{
|
||||
Random random = new(271828);
|
||||
for (int exponent = -1074; exponent <= 1023; exponent++)
|
||||
{
|
||||
double high = Math.ScaleB(1.0 + random.NextDouble(), exponent);
|
||||
double denseLow = Math.ScaleB(random.NextDouble(), exponent - 53);
|
||||
foreach (double low in new[] { 0.0, denseLow, -denseLow, double.Epsilon, -double.Epsilon })
|
||||
{
|
||||
DoubleDouble value = DoubleDouble.FromComponents(high, low);
|
||||
AssertExactText(value);
|
||||
AssertExactText(-value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("G0")]
|
||||
[InlineData("g0")]
|
||||
@@ -138,4 +211,40 @@ public class DoubleDoubleFormattingTests
|
||||
{
|
||||
Should.Throw<FormatException>(() => DoubleDouble.One.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
private static void AssertExactText(DoubleDouble value)
|
||||
{
|
||||
string text = value.ToStringExact();
|
||||
text.Length.ShouldBeLessThanOrEqualTo(2048);
|
||||
text.ShouldNotContain("E");
|
||||
text.ShouldNotContain("e");
|
||||
int point = text.IndexOf('.', StringComparison.Ordinal);
|
||||
int places = point < 0 ? 0 : text.Length - point - 1;
|
||||
if (point >= 0)
|
||||
{
|
||||
text.ShouldNotEndWith("0");
|
||||
text.ShouldNotEndWith(".");
|
||||
}
|
||||
string digits = point < 0 ? text : text.Remove(point, 1);
|
||||
BigInteger coefficient = BigInteger.Parse(digits, CultureInfo.InvariantCulture);
|
||||
// Independently decode the input as integer multiples of 2^-1074 and
|
||||
// compare with the printed integer coefficient / 10^places exactly.
|
||||
BigInteger units = ExactFormattingUnits(value.High) + ExactFormattingUnits(value.Low);
|
||||
(coefficient << 1074).ShouldBe(units * BigInteger.Pow(10, places));
|
||||
DoubleDouble parsed = DoubleDouble.Parse(text, CultureInfo.InvariantCulture);
|
||||
BitConverter.DoubleToInt64Bits(parsed.High).ShouldBe(BitConverter.DoubleToInt64Bits(value.High));
|
||||
BitConverter.DoubleToInt64Bits(parsed.Low).ShouldBe(BitConverter.DoubleToInt64Bits(value.Low));
|
||||
}
|
||||
|
||||
private static BigInteger ExactFormattingUnits(double value)
|
||||
{
|
||||
long bits = BitConverter.DoubleToInt64Bits(value);
|
||||
int exponent = (int)((bits >> 52) & 0x7ff);
|
||||
BigInteger significand = bits & 0xfffffffffffffL;
|
||||
if (exponent != 0)
|
||||
{
|
||||
significand = (significand + (BigInteger.One << 52)) << (exponent - 1);
|
||||
}
|
||||
return bits < 0 ? -significand : significand;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +202,83 @@ public class PreciseMathSqrtTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1.0)]
|
||||
[InlineData(1.0)]
|
||||
public void ScalingPreservesRepresentableSparseRootCorrections(double sign)
|
||||
{
|
||||
// sqrt(2^1000 + d) = 2^500 + d/2^501 + O(d^2/2^1500).
|
||||
// Scaling d=+/-2^-174 down by 2^1000 erases it, but the root's
|
||||
// +/-2^-675 low is representable. Prove its rounding interval exactly.
|
||||
DoubleDouble input = DoubleDouble.FromComponents(Math.ScaleB(1.0, 1000), Math.ScaleB(sign, -174));
|
||||
AssertSparseRoot(input, Math.ScaleB(1.0, 500), Math.ScaleB(sign, -675));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SparseRootCorrectionsSurviveScalingAndCorrectionDivisionBoundaries()
|
||||
{
|
||||
// Exact squares with both even and odd high exponents. Include scaled
|
||||
// lows on both sides of the separate-correction dispatch, subnormal
|
||||
// scaled lows, fully erased scaled lows, and subnormal output lows.
|
||||
foreach (int exponent in new[] { 0, 2, 100, 1000, 1022 })
|
||||
{
|
||||
foreach (double rootMantissa in new[] { 1.0, 1.5 })
|
||||
{
|
||||
double high = Math.ScaleB(rootMantissa * rootMantissa, exponent);
|
||||
double rootHigh = Math.ScaleB(rootMantissa, exponent / 2);
|
||||
foreach (int gap in new[] { -1019, -1020, -1021, -1074, -1075, -1174 })
|
||||
{
|
||||
double low = Math.ScaleB(1.0, exponent + gap);
|
||||
double rootLow = Math.ScaleB(low, -(exponent / 2)) / (2.0 * rootMantissa);
|
||||
if (rootLow == 0.0)
|
||||
{
|
||||
continue; // This matrix targets representable nonzero corrections.
|
||||
}
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
// AssertSparseRoot independently proves this candidate's
|
||||
// rounding interval; the first-order formula is not an oracle.
|
||||
AssertSparseRoot(DoubleDouble.FromComponents(high, sign * low), rootHigh, sign * rootLow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SparseRootDispatchNeighborsRetainBothSignsWithoutDoubleCounting()
|
||||
{
|
||||
// With high=2^1000, lows around 2^-20 scale to the dispatch at 2^-1020.
|
||||
// Keep the predicted output normal so candidate scaling is exact.
|
||||
double threshold = Math.ScaleB(1.0, -20);
|
||||
foreach (double low in new[] { Math.BitDecrement(threshold), threshold, Math.BitIncrement(threshold) })
|
||||
{
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
DoubleDouble input = DoubleDouble.FromComponents(Math.ScaleB(1.0, 1000), sign * low);
|
||||
AssertSparseRoot(input, Math.ScaleB(1.0, 500), Math.ScaleB(sign * low, -501));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertSparseRoot(DoubleDouble input, double high, double low)
|
||||
{
|
||||
BigInteger x = (Units(input.High) + Units(input.Low)) << 1074;
|
||||
BigInteger center = Units(high) + Units(low);
|
||||
BigInteger previous = Units(high) + Units(Math.BitDecrement(low));
|
||||
BigInteger following = Units(high) + Units(Math.BitIncrement(low));
|
||||
// Strict bounds avoid relying on a tie rule or a rounded sqrt oracle.
|
||||
((previous + center) * (previous + center) < (x << 2)).ShouldBeTrue();
|
||||
((center + following) * (center + following) > (x << 2)).ShouldBeTrue();
|
||||
DoubleDouble actual = DoubleDouble.Sqrt(input);
|
||||
actual.High.ShouldBe(high);
|
||||
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(low));
|
||||
DoubleDouble facade = DDMath.Sqrt(input);
|
||||
BitConverter.DoubleToInt64Bits(facade.High).ShouldBe(BitConverter.DoubleToInt64Bits(actual.High));
|
||||
BitConverter.DoubleToInt64Bits(facade.Low).ShouldBe(BitConverter.DoubleToInt64Bits(actual.Low));
|
||||
AssertSqrtBound(input, actual);
|
||||
}
|
||||
|
||||
private static void AssertSqrtBound(DoubleDouble input, DoubleDouble actual)
|
||||
{
|
||||
DoubleDouble.IsFinite(actual).ShouldBeTrue();
|
||||
|
||||
@@ -87,6 +87,18 @@ public class ArithmeticBenchmarks
|
||||
return _doubleDoubleLeft * _doubleDoubleRight;
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true), BenchmarkCategory("Squaring")]
|
||||
public DoubleDouble DoubleDoubleSquareViaMultiply()
|
||||
{
|
||||
return _doubleDoubleLeft * _doubleDoubleLeft;
|
||||
}
|
||||
|
||||
[Benchmark, BenchmarkCategory("Squaring")]
|
||||
public DoubleDouble DoubleDoubleSquare()
|
||||
{
|
||||
return DoubleDouble.Square(_doubleDoubleLeft);
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true), BenchmarkCategory("Division")]
|
||||
public double DoubleDivide()
|
||||
{
|
||||
|
||||
@@ -109,6 +109,15 @@ The `DDMath` static class provides:
|
||||
- `Abs(DoubleDouble)`: preserves both components, maps either signed zero to
|
||||
positive zero and either infinity to positive infinity, and returns canonical NaN.
|
||||
It shares the existing `DoubleDouble.Abs` implementation.
|
||||
- `Square(DoubleDouble)`: forwards to the specialized `DoubleDouble.Square` kernel.
|
||||
It combines the equal cross terms with FMA, normalizes, then incorporates the
|
||||
low-component square without losing selected exactly representable tails.
|
||||
Either signed zero maps to positive zero, either infinity to positive infinity,
|
||||
and NaN to canonical NaN. It shares multiplication's allocating boundary fallback
|
||||
and tested `2^-100` relative error plus one minimum binary64 subnormal bound.
|
||||
Results can differ in low bits from `value * value`; neither route is universally
|
||||
correctly rounded. No speedup has been measured, and existing kernels have not
|
||||
been rewritten to use it.
|
||||
- `Reciprocal(DoubleDouble)`: returns exactly the same high and low component bits
|
||||
as `1.0 / value`. It specializes scalar/DD division for a numerator of one,
|
||||
omitting redundant numerator checks and sharing the finite scalar-numerator kernel,
|
||||
@@ -118,7 +127,11 @@ The `DDMath` static class provides:
|
||||
No speedup over scalar/DD division has been measured.
|
||||
- `Sqrt(DoubleDouble)`: uses power-of-two scaling and an FMA-based Newton correction
|
||||
to retain extended precision, including for subnormal inputs, without squaring
|
||||
an unscaled estimate near the exponent limits. Signed zero and positive infinity
|
||||
an unscaled estimate near the exponent limits. Sparse lows endangered by input
|
||||
scaling or correction division are applied separately with tracked exponents:
|
||||
for example, `sqrt((2^1000, 2^-174))` retains the low component `2^-675`.
|
||||
This exceptional correction can reach the allocating boundary machinery;
|
||||
ordinary inputs retain the original scaled correction path. Signed zero and positive infinity
|
||||
are preserved; negative nonzero inputs and NaN return canonical NaN.
|
||||
- `InvSqrt(DoubleDouble)`: computes the reciprocal square root with power-of-two
|
||||
scaling and a compensated Newton step, avoiding double-double division and its
|
||||
@@ -195,6 +208,7 @@ The `DDMath` static class provides:
|
||||
using Just.PreciseMath;
|
||||
|
||||
DoubleDouble root = DoubleDouble.Sqrt(new DoubleDouble(2.0));
|
||||
DoubleDouble square = DoubleDouble.Square(root);
|
||||
DoubleDouble inverseRoot = DoubleDouble.InvSqrt(new DoubleDouble(2.0));
|
||||
DoubleDouble cubeRoot = DoubleDouble.Cbrt(new DoubleDouble(-8.0));
|
||||
DoubleDouble distance = DoubleDouble.Hypot(new DoubleDouble(3.0), new DoubleDouble(4.0));
|
||||
@@ -227,6 +241,9 @@ squares/powers of four. Square-root tests also bracket exact root-rounding midpo
|
||||
both suites exercise half-ulp low-component normalization boundaries.
|
||||
This is a tested approximate-accuracy contract, not exhaustive coverage
|
||||
of all component pairs or a guarantee of correctly rounded results.
|
||||
Square-root tests separately verify selected sparse output residuals with exact
|
||||
squared midpoint inequalities, including either low sign, dispatch neighbors,
|
||||
and corrections that become representable only after rescaling.
|
||||
|
||||
Cube-root and nth-root tests check `2^-100` relative error for finite nonzero
|
||||
inputs with degree magnitude at least two. Small degrees use exact integer-power
|
||||
@@ -306,6 +323,14 @@ sampled error bounds does not guarantee correct range decisions for every input.
|
||||
- `TryFormat(Span<char>, ...)` implements `ISpanFormattable` with the same formats.
|
||||
It currently allocates via `ToString`; insufficient space returns `false`, writes
|
||||
zero characters, and leaves the destination unchanged.
|
||||
- `ToStringExact()` emits the complete decimal expansion of the stored component
|
||||
sum, without rounding, scientific notation, or unnecessary fractional zeros.
|
||||
Unlike ordinary `ToString`, it always uses **invariant culture**. It preserves
|
||||
negative zero and emits `NaN`, `Infinity`, or `-Infinity` for special values.
|
||||
It allocates through exact integer arithmetic; sparse pairs can require over
|
||||
a thousand characters. Use `DoubleDouble.Parse(text, CultureInfo.InvariantCulture)`
|
||||
to recover the components. This is not a shortest-round-trip format and does not
|
||||
change the standard formats' 999-digit precision limit or the parser's length limit.
|
||||
|
||||
`DoubleDouble` implements `ISignedNumber<DoubleDouble>`, including the inherited
|
||||
`INumberBase` contracts: binary radix, classification, absolute value, magnitude
|
||||
@@ -374,7 +399,7 @@ bool success = DoubleDouble.TryParse("1.25e-2".AsSpan(), CultureInfo.InvariantCu
|
||||
Natural `DDMath.Log`, the complete exponential family on `DoubleDouble`, and all
|
||||
three `DDMath.Pow` overloads are implemented.
|
||||
Logarithms in other bases, generic-math interfaces beyond `ISignedNumber`,
|
||||
`IFloatingPointConstants`, `IRootFunctions`, and `IExponentialFunctions`, additional text formats/general
|
||||
`IFloatingPointConstants`, `IRootFunctions`, and `IExponentialFunctions`, additional text formats/shortest-
|
||||
round-trip formatting, and non-arithmetic performance benchmarks remain deferred.
|
||||
Replacing allocating arithmetic boundary fallbacks is also deferred; the current
|
||||
`BigInteger` paths remain in place. That optimization does not require removing
|
||||
@@ -400,6 +425,8 @@ on CI workflow runs.
|
||||
BenchmarkDotNet measures arithmetic throughput, dependent-chain latency, and allocations,
|
||||
including comparisons of `DoubleDouble`, `decimal`, and `double`, mixed scalar operations,
|
||||
and exponent-boundary paths. These are performance measurements, not accuracy tests.
|
||||
Squaring cases compare `DoubleDouble.Square(value)` with `value * value`; their
|
||||
presence is not evidence of a speedup.
|
||||
|
||||
After the Release build above, run from the repository root:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user