using BenchmarkDotNet.Attributes; namespace Just.PreciseMath.Benchmarks; /// Compares a+b with a-(-b) using matched results and normalization paths. /// Includes array and loop costs. Negation and fixture validation are outside timing. [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); } }