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>