the first optimization round
.NET Test / .NET tests (push) Successful in 1m26s

This commit is contained in:
2026-09-14 13:58:04 +04:00
parent cb2524980d
commit 08036a31d5
12 changed files with 1316 additions and 49 deletions
@@ -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;
+127 -2
View File
@@ -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.