This commit is contained in:
@@ -35,22 +35,30 @@ public readonly partial struct DoubleDouble :
|
||||
{
|
||||
return new DoubleDouble(left._high + right._high);
|
||||
}
|
||||
if (Math.Max(Math.ILogB(left._high), Math.ILogB(right._high)) > 1020)
|
||||
if (!PreciseMathHelper.IsAdditionWithinFastRange(left._high) || !PreciseMathHelper.IsAdditionWithinFastRange(right._high))
|
||||
{
|
||||
return PreciseMathHelper.ArithmeticFromRatio(PreciseMathHelper.ArithmeticUnits(left) + PreciseMathHelper.ArithmeticUnits(right), BigInteger.One << 1074);
|
||||
return PreciseMathHelper.AddBoundary(left, right);
|
||||
}
|
||||
|
||||
(double high, double highError) = PreciseMathHelper.TwoAdd(left._high, right._high);
|
||||
(double low, double lowError) = PreciseMathHelper.TwoAdd(left._low, right._low);
|
||||
(double middle, double middleError) = PreciseMathHelper.TwoAdd(highError, low);
|
||||
(double sum, double sumError) = PreciseMathHelper.TwoAdd(high, middle);
|
||||
return FromComponents(sum, sumError + (middleError + lowError));
|
||||
return PreciseMathHelper.AddFinite(left._high, left._low, right._high, right._low);
|
||||
}
|
||||
|
||||
/// <summary>Subtracts normalized expansions.</summary>
|
||||
public static DoubleDouble operator -(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
return left + (-right);
|
||||
if (!IsFinite(left) || !IsFinite(right) || (left._high == 0.0 && right._high == 0.0))
|
||||
{
|
||||
return new DoubleDouble(left._high - right._high);
|
||||
}
|
||||
if (!PreciseMathHelper.IsAdditionWithinFastRange(left._high) || !PreciseMathHelper.IsAdditionWithinFastRange(right._high))
|
||||
{
|
||||
return PreciseMathHelper.AddBoundary(left, -right);
|
||||
}
|
||||
|
||||
// With a canonical left low (never -0), the low TwoSum absorbs the
|
||||
// negated right zero low without changing either output component's bits.
|
||||
// Avoid unary negation's intermediate canonicalization on this finite path.
|
||||
return PreciseMathHelper.AddFinite(left._high, left._low, -right._high, -right._low);
|
||||
}
|
||||
|
||||
/// <summary>Multiplies expansions using an FMA product residual and cross terms.</summary>
|
||||
@@ -61,17 +69,20 @@ public readonly partial struct DoubleDouble :
|
||||
{
|
||||
return new DoubleDouble(left._high * right._high);
|
||||
}
|
||||
int exponent = Math.ILogB(left._high) + Math.ILogB(right._high);
|
||||
if (exponent < -900 || exponent > 900)
|
||||
if (!PreciseMathHelper.IsMultiplicationWithinFastRange(left._high, right._high))
|
||||
{
|
||||
return PreciseMathHelper.ArithmeticFromRatio(PreciseMathHelper.ArithmeticUnits(left) * PreciseMathHelper.ArithmeticUnits(right), BigInteger.One << 2148);
|
||||
return PreciseMathHelper.MultiplyBoundary(left, right);
|
||||
}
|
||||
|
||||
(double product, double error) = PreciseMathHelper.TwoMultiply(left._high, right._high);
|
||||
error = Math.FusedMultiplyAdd(left._high, right._low, error);
|
||||
error = Math.FusedMultiplyAdd(left._low, right._high, error);
|
||||
error = Math.FusedMultiplyAdd(left._low, right._low, error);
|
||||
return FromComponents(product, error);
|
||||
// With u = 2^-53, normalized inputs and the exponent-sum guard give
|
||||
// |error| < 4u*|product|, including rounding at the subnormal floor.
|
||||
// The product is normal and nonzero; its corrected sum remains finite.
|
||||
(double high, double low) = PreciseMathHelper.TwoQuickAdd(product, error);
|
||||
return new DoubleDouble(high, low == 0.0 ? 0.0 : low);
|
||||
}
|
||||
|
||||
/// <summary>Divides expansions using a quotient estimate and two residual corrections.</summary>
|
||||
@@ -82,11 +93,9 @@ public readonly partial struct DoubleDouble :
|
||||
{
|
||||
return new DoubleDouble(left._high / right._high);
|
||||
}
|
||||
int leftExponent = Math.ILogB(left._high);
|
||||
int rightExponent = Math.ILogB(right._high);
|
||||
if (Math.Abs(leftExponent) > 450 || Math.Abs(rightExponent) > 450)
|
||||
if (!PreciseMathHelper.IsDivisionWithinFastRange(left._high) || !PreciseMathHelper.IsDivisionWithinFastRange(right._high))
|
||||
{
|
||||
return PreciseMathHelper.ArithmeticFromRatio(PreciseMathHelper.ArithmeticUnits(left), PreciseMathHelper.ArithmeticUnits(right));
|
||||
return PreciseMathHelper.DivideBoundary(left, right);
|
||||
}
|
||||
|
||||
double quotient = left._high / right._high;
|
||||
@@ -129,10 +138,9 @@ public readonly partial struct DoubleDouble :
|
||||
{
|
||||
return new DoubleDouble(left._high * right);
|
||||
}
|
||||
int exponent = Math.ILogB(left._high) + Math.ILogB(right);
|
||||
if (exponent < -900 || exponent > 900)
|
||||
if (!PreciseMathHelper.IsMultiplicationWithinFastRange(left._high, right))
|
||||
{
|
||||
return PreciseMathHelper.ArithmeticFromRatio(PreciseMathHelper.ArithmeticUnits(left) * PreciseMathHelper.ArithmeticUnits(right), BigInteger.One << 2148);
|
||||
return PreciseMathHelper.MultiplyBoundary(left, right);
|
||||
}
|
||||
|
||||
(double product, double error) = PreciseMathHelper.TwoMultiply(left._high, right);
|
||||
@@ -156,11 +164,9 @@ public readonly partial struct DoubleDouble :
|
||||
{
|
||||
return new DoubleDouble(left._high / right);
|
||||
}
|
||||
int leftExponent = Math.ILogB(left._high);
|
||||
int rightExponent = Math.ILogB(right);
|
||||
if (Math.Abs(leftExponent) > 450 || Math.Abs(rightExponent) > 450)
|
||||
if (!PreciseMathHelper.IsDivisionWithinFastRange(left._high) || !PreciseMathHelper.IsDivisionWithinFastRange(right))
|
||||
{
|
||||
return PreciseMathHelper.ArithmeticFromRatio(PreciseMathHelper.ArithmeticUnits(left), PreciseMathHelper.ArithmeticUnits(right));
|
||||
return PreciseMathHelper.DivideBoundary(left, right);
|
||||
}
|
||||
|
||||
double quotient = left._high / right;
|
||||
@@ -179,11 +185,9 @@ public readonly partial struct DoubleDouble :
|
||||
{
|
||||
return new DoubleDouble(left / right._high);
|
||||
}
|
||||
int leftExponent = Math.ILogB(left);
|
||||
int rightExponent = Math.ILogB(right._high);
|
||||
if (Math.Abs(leftExponent) > 450 || Math.Abs(rightExponent) > 450)
|
||||
if (!PreciseMathHelper.IsDivisionWithinFastRange(left) || !PreciseMathHelper.IsDivisionWithinFastRange(right._high))
|
||||
{
|
||||
return PreciseMathHelper.ArithmeticFromRatio(PreciseMathHelper.ArithmeticUnits(left), PreciseMathHelper.ArithmeticUnits(right));
|
||||
return PreciseMathHelper.DivideBoundary(left, right);
|
||||
}
|
||||
|
||||
double quotient = left / right._high;
|
||||
|
||||
@@ -2,6 +2,45 @@ namespace Just.PreciseMath;
|
||||
|
||||
internal static class PreciseMathHelper
|
||||
{
|
||||
// Finite operands only; callers retain their special-value/zero handling.
|
||||
// ILogB(value) <= 1020 is exactly biasedExponent <= 2043. Zero and
|
||||
// subnormal operands also qualify without computing their true exponents.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static bool IsAdditionWithinFastRange(double value)
|
||||
{
|
||||
int exponent = (int)((BitConverter.DoubleToUInt64Bits(value) >> 52) & 0x7ff);
|
||||
return exponent <= 2043;
|
||||
}
|
||||
|
||||
// Finite nonzero operands only. The original inclusive ILogB interval
|
||||
// [-450, 450] becomes [573, 1473] with the binary64 bias of 1023.
|
||||
// Unsigned subtraction rejects smaller exponents, including subnormals.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static bool IsDivisionWithinFastRange(double value)
|
||||
{
|
||||
int exponent = (int)((BitConverter.DoubleToUInt64Bits(value) >> 52) & 0x7ff);
|
||||
return unchecked((uint)(exponent - 573)) <= 900;
|
||||
}
|
||||
|
||||
// Finite nonzero operands only. For normal operands the exponent sum
|
||||
// interval [-900, 900] becomes [1146, 2946] after adding both biases.
|
||||
// A subnormal times a large normal can still be in range: preserve the
|
||||
// original ILogB calculation for those operands, not an allocating detour.
|
||||
// Both multiplication overloads reject nonfinite and zero operands first;
|
||||
// this predicate does not validate them (ILogB(0) is an integer sentinel).
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static bool IsMultiplicationWithinFastRange(double left, double right)
|
||||
{
|
||||
int leftExponent = (int)((BitConverter.DoubleToUInt64Bits(left) >> 52) & 0x7ff);
|
||||
int rightExponent = (int)((BitConverter.DoubleToUInt64Bits(right) >> 52) & 0x7ff);
|
||||
if (leftExponent == 0 || rightExponent == 0)
|
||||
{
|
||||
int exponent = Math.ILogB(left) + Math.ILogB(right);
|
||||
return exponent >= -900 && exponent <= 900;
|
||||
}
|
||||
return unchecked((uint)((leftExponent + rightExponent) - 1146)) <= 1800;
|
||||
}
|
||||
|
||||
// General TwoSum: no magnitude ordering required, but inputs, sum, and
|
||||
// intermediate subtractions must stay finite. Arithmetic callers bound the
|
||||
// exponents; arbitrary-component normalization uses magnitude ordering instead.
|
||||
@@ -47,6 +86,44 @@ internal static class PreciseMathHelper
|
||||
return (r, Math.FusedMultiplyAdd(a, a, -r));
|
||||
}
|
||||
|
||||
// Normalized finite operands passing the addition range guard, excluding two
|
||||
// zero highs. The left low must be canonical (nonzero or +0, never -0).
|
||||
// The right low may be a negated zero: under that left-low precondition,
|
||||
// TwoAdd(leftLow, +0) and TwoAdd(leftLow, -0) have bit-identical outputs.
|
||||
// Keep all four transforms and grouping.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static DoubleDouble AddFinite(double leftHigh, double leftLow, double rightHigh, double rightLow)
|
||||
{
|
||||
(double high, double highError) = TwoAdd(leftHigh, rightHigh);
|
||||
(double low, double lowError) = TwoAdd(leftLow, rightLow);
|
||||
(double middle, double middleError) = TwoAdd(highError, low);
|
||||
(double sum, double sumError) = TwoAdd(high, middle);
|
||||
// Input highs are < 2^1021 and lows <= 2^967 in magnitude. These
|
||||
// transforms keep |sum| <= 2^1022 and the correction <= 2^970,
|
||||
// so finite-only normalization is safe even under cancellation.
|
||||
return NormalizeFinite(sum, sumError + (middleError + lowError));
|
||||
}
|
||||
|
||||
// Both components and their rounded sum must be finite. Unlike QuickTwoSum,
|
||||
// this entry point permits either magnitude order, including cancellation.
|
||||
// AddFinite establishes these bounds; this helper does not validate them or
|
||||
// canonicalize NaN/infinity. Use DoubleDouble.FromComponents for arbitrary pairs.
|
||||
// Retain the high zero's sign when low is zero, as FromComponents does.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static DoubleDouble NormalizeFinite(double high, double low)
|
||||
{
|
||||
if (low == 0.0)
|
||||
{
|
||||
return new DoubleDouble(high, 0.0);
|
||||
}
|
||||
|
||||
double sum = high + low;
|
||||
double error = Math.Abs(high) >= Math.Abs(low)
|
||||
? low - (sum - high)
|
||||
: high - (sum - low);
|
||||
return new DoubleDouble(sum, error == 0.0 ? 0.0 : error);
|
||||
}
|
||||
|
||||
// The first two arguments are normalized components (a negated zero low is
|
||||
// also allowed). Sharing this path preserves both subtraction orders without
|
||||
// constructing a temporary expansion for the scalar or the negated operand.
|
||||
@@ -56,9 +133,9 @@ internal static class PreciseMathHelper
|
||||
{
|
||||
return new DoubleDouble(high + value);
|
||||
}
|
||||
if (Math.Max(Math.ILogB(high), Math.ILogB(value)) > 1020)
|
||||
if (!IsAdditionWithinFastRange(high) || !IsAdditionWithinFastRange(value))
|
||||
{
|
||||
return ArithmeticFromRatio(ArithmeticUnits(high) + ArithmeticUnits(low) + ArithmeticUnits(value), BigInteger.One << 1074);
|
||||
return AddScalarBoundary(high, low, value);
|
||||
}
|
||||
|
||||
(double sum, double error) = TwoAdd(high, value);
|
||||
@@ -69,6 +146,54 @@ internal static class PreciseMathHelper
|
||||
return new DoubleDouble(result, residual == 0.0 ? 0.0 : residual);
|
||||
}
|
||||
|
||||
// Keep the complete BigInteger expressions out of ordinary arithmetic bodies,
|
||||
// including operand conversion and denominator construction. NoInlining isolates
|
||||
// this setup even when the public operators are inlined by their callers.
|
||||
// All inputs must be finite and normalized; division denominators must be nonzero.
|
||||
// Callers retain the special-value, signed-zero, and exponent-range dispatch.
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal static DoubleDouble AddBoundary(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
return ArithmeticFromRatio(ArithmeticUnits(left) + ArithmeticUnits(right), BigInteger.One << 1074);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static DoubleDouble AddScalarBoundary(double high, double low, double value)
|
||||
{
|
||||
// As in AddScalar, high/low are normalized but a negated zero low is allowed.
|
||||
return ArithmeticFromRatio(ArithmeticUnits(high) + ArithmeticUnits(low) + ArithmeticUnits(value), BigInteger.One << 1074);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal static DoubleDouble MultiplyBoundary(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
return ArithmeticFromRatio(ArithmeticUnits(left) * ArithmeticUnits(right), BigInteger.One << 2148);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal static DoubleDouble MultiplyBoundary(DoubleDouble left, double right)
|
||||
{
|
||||
return ArithmeticFromRatio(ArithmeticUnits(left) * ArithmeticUnits(right), BigInteger.One << 2148);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal static DoubleDouble DivideBoundary(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
return ArithmeticFromRatio(ArithmeticUnits(left), ArithmeticUnits(right));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal static DoubleDouble DivideBoundary(DoubleDouble left, double right)
|
||||
{
|
||||
return ArithmeticFromRatio(ArithmeticUnits(left), ArithmeticUnits(right));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal static DoubleDouble DivideBoundary(double left, DoubleDouble right)
|
||||
{
|
||||
return ArithmeticFromRatio(ArithmeticUnits(left), ArithmeticUnits(right));
|
||||
}
|
||||
|
||||
// The boundary path uses bounded binary integers (at most about 4200 bits), not
|
||||
// arbitrary-precision storage. It avoids overflow and double rounding in EFTs
|
||||
// at the binary64 exponent limits. The common path remains allocation-free.
|
||||
|
||||
Reference in New Issue
Block a user