square function and some sanity checks
.NET Test / .NET tests (push) Successful in 1m35s

This commit is contained in:
2026-09-18 16:59:28 +04:00
parent 5797bf4884
commit 620faca7eb
10 changed files with 455 additions and 28 deletions
+9 -22
View File
@@ -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()
{
+29 -2
View File
@@ -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: