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
@@ -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);
}
}
@@ -0,0 +1,114 @@
using BenchmarkDotNet.Attributes;
namespace Just.PreciseMath.Benchmarks;
/// <summary>Isolates ordinary/fallback exponent transitions, not special-value handling.</summary>
[MemoryDiagnoser]
[CategoriesColumn]
public class ArithmeticBoundaryBenchmarks
{
private DoubleDouble _addLeft;
private DoubleDouble _addRight;
private DoubleDouble _multiplyLeft;
private DoubleDouble _divideLeft;
private DoubleDouble _factor;
[Params(false, true)]
public bool Fallback { get; set; }
[GlobalSetup]
public void Setup()
{
int offset = Fallback ? 1 : 0;
_addLeft = Pair(1020 + offset);
_addRight = Pair(1019);
_multiplyLeft = Pair(900 + offset);
_divideLeft = Pair(450 + offset);
_factor = DoubleDouble.FromComponents(1.125, Math.ScaleB(1.0, -56));
// Pin dispatch assumptions to the current source guards. Both signs/orders
// remain finite; boundary timing is intentionally separate from ordinary data.
if ((Math.ILogB(_addLeft.High) > 1020) != Fallback
|| (Math.ILogB(_multiplyLeft.High) + Math.ILogB(_factor.High) > 900) != Fallback
|| (Math.Abs(Math.ILogB(_divideLeft.High)) > 450) != Fallback)
{
throw new InvalidOperationException("Boundary fixture does not select the requested guard branch.");
}
}
private static DoubleDouble Pair(int exponent)
{
return DoubleDouble.FromComponents(Math.ScaleB(1.0, exponent), Math.ScaleB(1.0, exponent - 56));
}
[Benchmark, BenchmarkCategory("Addition")]
public DoubleDouble DDAdd()
{
return _addLeft + _addRight;
}
[Benchmark, BenchmarkCategory("Addition")]
public DoubleDouble DDScalarAdd()
{
return _addLeft + _addRight.High;
}
[Benchmark, BenchmarkCategory("Addition")]
public DoubleDouble ScalarDDAdd()
{
return _addLeft.High + _addRight;
}
[Benchmark, BenchmarkCategory("Subtraction")]
public DoubleDouble DDSubtract()
{
return _addLeft - _addRight;
}
[Benchmark, BenchmarkCategory("Subtraction")]
public DoubleDouble DDScalarSubtract()
{
return _addLeft - _addRight.High;
}
[Benchmark, BenchmarkCategory("Subtraction")]
public DoubleDouble ScalarDDSubtract()
{
return _addLeft.High - _addRight;
}
[Benchmark, BenchmarkCategory("Multiplication")]
public DoubleDouble DDMultiply()
{
return _multiplyLeft * _factor;
}
[Benchmark, BenchmarkCategory("Multiplication")]
public DoubleDouble DDScalarMultiply()
{
return _multiplyLeft * _factor.High;
}
[Benchmark, BenchmarkCategory("Multiplication")]
public DoubleDouble ScalarDDMultiply()
{
return _multiplyLeft.High * _factor;
}
[Benchmark, BenchmarkCategory("Division")]
public DoubleDouble DDDivide()
{
return _divideLeft / _factor;
}
[Benchmark, BenchmarkCategory("Division")]
public DoubleDouble DDScalarDivide()
{
return _divideLeft / _factor.High;
}
[Benchmark, BenchmarkCategory("Division")]
public DoubleDouble ScalarDDDivide()
{
return _factor.High / _divideLeft;
}
}
@@ -0,0 +1,129 @@
namespace Just.PreciseMath.Benchmarks;
// Input construction stays outside timed benchmark methods.
internal sealed class ArithmeticInputs
{
internal const int Count = 64;
internal DoubleDouble[] Left { get; } = new DoubleDouble[Count];
internal DoubleDouble[] Right { get; } = new DoubleDouble[Count];
internal static ArithmeticInputs Create(string scenario, bool chain)
{
if (chain && scenario is not ("BothResidual" or "Mixed"))
{
throw new ArgumentOutOfRangeException(nameof(scenario));
}
if (scenario is not ("BinaryExact" or "DecimalResidual" or "BothResidual" or "Mixed" or "SignsAndScales" or "Cancellation"))
{
throw new ArgumentOutOfRangeException(nameof(scenario));
}
ArithmeticInputs inputs = new();
for (int i = 0; i < Count; i++)
{
// Exact dyadic construction, not decimal-to-double residual estimation.
double high = 1.25 + ((i % 7) / 32.0);
double low = Math.ScaleB(1.0, -56);
DoubleDouble left = DoubleDouble.FromComponents(high, low);
DoubleDouble right = DoubleDouble.FromComponents(1.0 + (((i % 5) - 2) / 128.0), -low);
if (!chain)
{
switch (scenario)
{
case "BinaryExact":
left = new DoubleDouble(high);
right = new DoubleDouble(0.75);
break;
case "DecimalResidual":
left = (DoubleDouble)1.1m;
right = new DoubleDouble(0.75);
break;
case "SignsAndScales":
int exponent = ((i % 7) - 3) * 40;
double sign = i % 2 == 0 ? 1.0 : -1.0;
left = DoubleDouble.FromComponents(Math.ScaleB(sign * high, exponent), Math.ScaleB(low, exponent));
right = DoubleDouble.FromComponents(Math.ScaleB(1.125, -exponent), Math.ScaleB(-low, -exponent));
break;
case "Cancellation":
// Alternate near cancellation in addition and subtraction.
right = DoubleDouble.FromComponents(i % 2 == 0 ? -high : high, -low / 2.0);
break;
}
}
if (scenario == "Mixed")
{
// Alternate both residuals, right zero-low, left zero-low, both zero-low.
if (i % 4 is 2 or 3)
{
left = new DoubleDouble(left.High);
}
if (i % 4 is 1 or 3)
{
right = new DoubleDouble(right.High);
}
}
inputs.Left[i] = left;
inputs.Right[i] = right;
}
inputs.Validate(scenario, chain);
return inputs;
}
private void Validate(string scenario, bool chain)
{
for (int i = 0; i < Count; i++)
{
ValidateOrdinary(Left[i]);
ValidateOrdinary(Right[i]);
bool expectedLeftLow = scenario != "BinaryExact" && (scenario != "Mixed" || i % 4 < 2);
bool expectedRightLow = scenario is not ("BinaryExact" or "DecimalResidual") && (scenario != "Mixed" || i % 2 == 0);
if ((Left[i].Low != 0.0) != expectedLeftLow || (Right[i].Low != 0.0) != expectedRightLow)
{
throw new InvalidOperationException("Benchmark residual shape does not match its scenario.");
}
}
if (!chain)
{
return;
}
// Setup-only guard against a chain drifting into special or exponent-boundary
// paths. This is fixture validation, not an independent accuracy oracle.
for (int operation = 0; operation < 12; operation++)
{
DoubleDouble value = Left[0];
for (int i = 0; i < Count; i++)
{
DoubleDouble right = Right[i];
value = operation switch
{
0 => value + right,
1 => value - right,
2 => value * right,
3 => value / right,
4 => value + right.High,
5 => value - right.High,
6 => value * right.High,
7 => value / right.High,
8 => right.High + value,
9 => right.High - value,
10 => right.High * value,
11 => right.High / value,
_ => throw new InvalidOperationException()
};
ValidateOrdinary(value);
}
}
}
private static void ValidateOrdinary(DoubleDouble value)
{
if (!DoubleDouble.IsFinite(value) || value.High == 0.0 || Math.Abs(Math.ILogB(value.High)) > 200
|| DoubleDouble.FromComponents(value.High, value.Low) != value
|| (value.Low == 0.0 && BitConverter.DoubleToInt64Bits(value.Low) != 0))
{
throw new InvalidOperationException("Benchmark input/chain left the normalized ordinary finite range.");
}
}
}
@@ -0,0 +1,157 @@
using BenchmarkDotNet.Attributes;
namespace Just.PreciseMath.Benchmarks;
/// <summary>Measures bounded dependent chains; each invocation resets the seed.</summary>
[MemoryDiagnoser]
[CategoriesColumn]
public class ArithmeticLatencyBenchmarks
{
private DoubleDouble[] _left = [];
private DoubleDouble[] _right = [];
private double[] _scalarRight = [];
[Params("BothResidual", "Mixed")]
public string Scenario { get; set; } = "BothResidual";
[GlobalSetup]
public void Setup()
{
ArithmeticInputs inputs = ArithmeticInputs.Create(Scenario, true);
_left = inputs.Left;
_right = inputs.Right;
_scalarRight = Array.ConvertAll(_right, value => value.High);
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Addition")]
public DoubleDouble DDAdd()
{
DoubleDouble value = _left[0];
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
value = value + _right[i];
}
return value;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Addition")]
public DoubleDouble DDScalarAdd()
{
DoubleDouble value = _left[0];
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
value = value + _scalarRight[i];
}
return value;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Addition")]
public DoubleDouble ScalarDDAdd()
{
DoubleDouble value = _left[0];
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
value = _scalarRight[i] + value;
}
return value;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Subtraction")]
public DoubleDouble DDSubtract()
{
DoubleDouble value = _left[0];
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
value = value - _right[i];
}
return value;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Subtraction")]
public DoubleDouble DDScalarSubtract()
{
DoubleDouble value = _left[0];
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
value = value - _scalarRight[i];
}
return value;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Subtraction")]
public DoubleDouble ScalarDDSubtract()
{
DoubleDouble value = _left[0];
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
value = _scalarRight[i] - value;
}
return value;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Multiplication")]
public DoubleDouble DDMultiply()
{
DoubleDouble value = _left[0];
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
value = value * _right[i];
}
return value;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Multiplication")]
public DoubleDouble DDScalarMultiply()
{
DoubleDouble value = _left[0];
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
value = value * _scalarRight[i];
}
return value;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Multiplication")]
public DoubleDouble ScalarDDMultiply()
{
DoubleDouble value = _left[0];
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
value = _scalarRight[i] * value;
}
return value;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Division")]
public DoubleDouble DDDivide()
{
DoubleDouble value = _left[0];
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
value = value / _right[i];
}
return value;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Division")]
public DoubleDouble DDScalarDivide()
{
DoubleDouble value = _left[0];
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
value = value / _scalarRight[i];
}
return value;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Division")]
public DoubleDouble ScalarDDDivide()
{
DoubleDouble value = _left[0];
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
value = _scalarRight[i] / value;
}
return value;
}
}
@@ -0,0 +1,189 @@
using BenchmarkDotNet.Attributes;
namespace Just.PreciseMath.Benchmarks;
/// <summary>Measures independent array operations, including loads, stores, and loop overhead.</summary>
[MemoryDiagnoser]
[CategoriesColumn]
public class ArithmeticThroughputBenchmarks
{
private DoubleDouble[] _left = [];
private DoubleDouble[] _right = [];
private double[] _scalarLeft = [];
private double[] _scalarRight = [];
private readonly DoubleDouble[] _results = new DoubleDouble[ArithmeticInputs.Count];
private readonly double[] _doubleResults = new double[ArithmeticInputs.Count];
[Params("BinaryExact", "DecimalResidual", "BothResidual", "Mixed", "SignsAndScales", "Cancellation")]
public string Scenario { get; set; } = "BothResidual";
[GlobalSetup]
public void Setup()
{
ArithmeticInputs inputs = ArithmeticInputs.Create(Scenario, false);
_left = inputs.Left;
_right = inputs.Right;
_scalarLeft = Array.ConvertAll(_left, value => value.High);
_scalarRight = Array.ConvertAll(_right, value => value.High);
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Addition")]
public DoubleDouble[] DDAdd()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_results[i] = _left[i] + _right[i];
}
return _results;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Addition")]
public DoubleDouble[] DDScalarAdd()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_results[i] = _left[i] + _scalarRight[i];
}
return _results;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Addition")]
public DoubleDouble[] ScalarDDAdd()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_results[i] = _scalarLeft[i] + _right[i];
}
return _results;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Addition")]
public double[] DoubleAdd()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_doubleResults[i] = _scalarLeft[i] + _scalarRight[i];
}
return _doubleResults;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Subtraction")]
public DoubleDouble[] DDSubtract()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_results[i] = _left[i] - _right[i];
}
return _results;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Subtraction")]
public DoubleDouble[] DDScalarSubtract()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_results[i] = _left[i] - _scalarRight[i];
}
return _results;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Subtraction")]
public DoubleDouble[] ScalarDDSubtract()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_results[i] = _scalarLeft[i] - _right[i];
}
return _results;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Subtraction")]
public double[] DoubleSubtract()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_doubleResults[i] = _scalarLeft[i] - _scalarRight[i];
}
return _doubleResults;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Multiplication")]
public DoubleDouble[] DDMultiply()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_results[i] = _left[i] * _right[i];
}
return _results;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Multiplication")]
public DoubleDouble[] DDScalarMultiply()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_results[i] = _left[i] * _scalarRight[i];
}
return _results;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Multiplication")]
public DoubleDouble[] ScalarDDMultiply()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_results[i] = _scalarLeft[i] * _right[i];
}
return _results;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Multiplication")]
public double[] DoubleMultiply()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_doubleResults[i] = _scalarLeft[i] * _scalarRight[i];
}
return _doubleResults;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Division")]
public DoubleDouble[] DDDivide()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_results[i] = _left[i] / _right[i];
}
return _results;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Division")]
public DoubleDouble[] DDScalarDivide()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_results[i] = _left[i] / _scalarRight[i];
}
return _results;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Division")]
public DoubleDouble[] ScalarDDDivide()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_results[i] = _scalarLeft[i] / _right[i];
}
return _results;
}
[Benchmark(OperationsPerInvoke = ArithmeticInputs.Count), BenchmarkCategory("Division")]
public double[] DoubleDivide()
{
for (int i = 0; i < ArithmeticInputs.Count; i++)
{
_doubleResults[i] = _scalarLeft[i] / _scalarRight[i];
}
return _doubleResults;
}
}