This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Just.PreciseMath.Benchmarks;
|
||||
|
||||
/// <summary>Compares a+b with a-(-b) using matched results and normalization paths.</summary>
|
||||
/// <remarks>Includes array and loop costs. Negation and fixture validation are outside timing.</remarks>
|
||||
[MemoryDiagnoser]
|
||||
public class AddSubtractMatchedBenchmarks
|
||||
{
|
||||
private const int Count = 64;
|
||||
private readonly DoubleDouble[] _left = new DoubleDouble[Count];
|
||||
private readonly DoubleDouble[] _right = new DoubleDouble[Count];
|
||||
private readonly DoubleDouble[] _results = new DoubleDouble[Count];
|
||||
|
||||
[Params("ZeroCorrection", "NonzeroCorrection", "Cancellation", "MixedCorrection")]
|
||||
public string Scenario { get; set; } = "NonzeroCorrection";
|
||||
|
||||
[GlobalSetup(Target = nameof(Add))]
|
||||
public void SetupAdd()
|
||||
{
|
||||
Setup(false);
|
||||
}
|
||||
|
||||
[GlobalSetup(Target = nameof(Subtract))]
|
||||
public void SetupSubtract()
|
||||
{
|
||||
Setup(true);
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = Count)]
|
||||
public DoubleDouble[] Add()
|
||||
{
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
_results[i] = _left[i] + _right[i];
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = Count)]
|
||||
public DoubleDouble[] Subtract()
|
||||
{
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
_results[i] = _left[i] - _right[i];
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
|
||||
private void Setup(bool subtract)
|
||||
{
|
||||
if (Scenario is not ("ZeroCorrection" or "NonzeroCorrection" or "Cancellation" or "MixedCorrection"))
|
||||
{
|
||||
throw new InvalidOperationException($"Unknown matched add/subtract scenario: {Scenario}.");
|
||||
}
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
// Cycle both high signs and moderate scales. High sums/differences
|
||||
// are exact: the significands are small integer multiples of 1/128.
|
||||
int exponent = (((i / 4) % 3) - 1) * 80;
|
||||
double leftSign = i % 2 == 0 ? 1.0 : -1.0;
|
||||
double rightSign = i % 4 < 2 ? 1.0 : -1.0;
|
||||
double leftHigh = leftSign * Math.ScaleB(1.25 + ((i % 7) / 32.0), exponent);
|
||||
double rightHigh = rightSign * Math.ScaleB(1.0 + (((i % 5) - 2) / 128.0), exponent);
|
||||
bool zeroCorrection = Scenario == "ZeroCorrection" || (Scenario == "MixedCorrection" && i % 3 == 0);
|
||||
double leftLow;
|
||||
double rightLow;
|
||||
double expectedHigh;
|
||||
double expectedLow;
|
||||
if (Scenario == "Cancellation")
|
||||
{
|
||||
// Equal/opposite highs leave the exact normalized expansion
|
||||
// (sign*2^(e-54), sign*2^(e-108)), not a binary64-only result.
|
||||
rightHigh = -leftHigh;
|
||||
leftLow = leftSign * Math.ScaleB(1.0, exponent - 54);
|
||||
rightLow = leftSign * Math.ScaleB(1.0, exponent - 108);
|
||||
expectedHigh = leftLow;
|
||||
expectedLow = rightLow;
|
||||
}
|
||||
else
|
||||
{
|
||||
leftLow = leftSign * Math.ScaleB(1.0, exponent - 80);
|
||||
rightLow = zeroCorrection ? -leftLow : leftLow / 2.0;
|
||||
expectedHigh = leftHigh + rightHigh;
|
||||
// This exact low sum is far below half an ulp of the high sum.
|
||||
expectedLow = zeroCorrection ? 0.0 : leftSign * Math.ScaleB(3.0, exponent - 81);
|
||||
}
|
||||
|
||||
DoubleDouble left = DoubleDouble.FromComponents(leftHigh, leftLow);
|
||||
DoubleDouble right = DoubleDouble.FromComponents(rightHigh, rightLow);
|
||||
DoubleDouble negativeRight = -right;
|
||||
CheckComponents(left, leftHigh, leftLow, i, "left input");
|
||||
CheckComponents(right, rightHigh, rightLow, i, "right input");
|
||||
CheckComponents(negativeRight, -rightHigh, -rightLow, i, "negated right input");
|
||||
if (!DoubleDouble.IsFinite(left) || !DoubleDouble.IsFinite(right)
|
||||
|| left.High == 0.0 || right.High == 0.0
|
||||
|| Math.Abs(Math.ILogB(left.High)) > 200 || Math.Abs(Math.ILogB(right.High)) > 200)
|
||||
{
|
||||
throw new InvalidOperationException($"{Scenario}[{i}] is outside the ordinary finite input range.");
|
||||
}
|
||||
// Independent dyadic expectations, not one DD operator as the other's oracle.
|
||||
CheckComponents(left + right, expectedHigh, expectedLow, i, "addition");
|
||||
CheckComponents(left - negativeRight, expectedHigh, expectedLow, i, "subtraction");
|
||||
// Both operators feed these same effective components into AddFinite.
|
||||
if ((FinalCorrection(left, right) == 0.0) != zeroCorrection)
|
||||
{
|
||||
throw new InvalidOperationException($"{Scenario}[{i}] does not select the expected normalization path.");
|
||||
}
|
||||
_left[i] = left;
|
||||
// Target-specific setup keeps array fields and timed memory access identical.
|
||||
_right[i] = subtract ? negativeRight : right;
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckComponents(DoubleDouble actual, double high, double low, int index, string operation)
|
||||
{
|
||||
if (BitConverter.DoubleToInt64Bits(actual.High) != BitConverter.DoubleToInt64Bits(high)
|
||||
|| BitConverter.DoubleToInt64Bits(actual.Low) != BitConverter.DoubleToInt64Bits(low))
|
||||
{
|
||||
throw new InvalidOperationException($"{Scenario}[{index}] has unexpected {operation} components.");
|
||||
}
|
||||
}
|
||||
|
||||
// Setup-only characterization, not an accuracy oracle. Keep this path check
|
||||
// synchronized with AddFinite if the production kernel changes in the future.
|
||||
private static double FinalCorrection(DoubleDouble left, DoubleDouble 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 _, double sumError) = PreciseMathHelper.TwoAdd(high, middle);
|
||||
return sumError + (middleError + lowError);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user