This commit is contained in:
+164
@@ -0,0 +1,164 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using Just.PreciseMath.PerformanceComparison.Reference;
|
||||
|
||||
namespace Just.PreciseMath.PerformanceComparison;
|
||||
|
||||
/// <summary>
|
||||
/// Compares the four arithmetic operators on the same ordinary finite operands across
|
||||
/// binary64, the <c>Just.PreciseMath</c> double-double, and the blog reference
|
||||
/// implementation. Every category measures one operator, and its binary64 method is
|
||||
/// the baseline, so the ratio columns read as "times the cost of a double".
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Operands are independent array elements, so the measurements include loads, stores,
|
||||
/// and loop overhead on purpose; input construction, conversion, and fixture validation
|
||||
/// stay outside the timed methods.
|
||||
/// </remarks>
|
||||
[MemoryDiagnoser]
|
||||
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
|
||||
[CategoriesColumn]
|
||||
public class ArithmeticComparisonBenchmarks
|
||||
{
|
||||
private double[] _doubleLeft = [];
|
||||
private double[] _doubleRight = [];
|
||||
private DoubleDouble[] _left = [];
|
||||
private DoubleDouble[] _right = [];
|
||||
private DoubleDoubleSample[] _sampleLeft = [];
|
||||
private DoubleDoubleSample[] _sampleRight = [];
|
||||
private readonly double[] _doubleResults = new double[PerformanceInputs.Count];
|
||||
private readonly DoubleDouble[] _results = new DoubleDouble[PerformanceInputs.Count];
|
||||
private readonly DoubleDoubleSample[] _sampleResults = new DoubleDoubleSample[PerformanceInputs.Count];
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
PerformanceInputs inputs = PerformanceInputs.Create();
|
||||
_doubleLeft = inputs.DoubleLeft;
|
||||
_doubleRight = inputs.DoubleRight;
|
||||
_left = inputs.Left;
|
||||
_right = inputs.Right;
|
||||
_sampleLeft = inputs.SampleLeft;
|
||||
_sampleRight = inputs.SampleRight;
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Addition")]
|
||||
public double[] DoubleAdd()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_doubleResults[i] = _doubleLeft[i] + _doubleRight[i];
|
||||
}
|
||||
return _doubleResults;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Addition")]
|
||||
public DoubleDouble[] DoubleDoubleAdd()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_results[i] = _left[i] + _right[i];
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Addition")]
|
||||
public DoubleDoubleSample[] DoubleDoubleSampleAdd()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_sampleResults[i] = _sampleLeft[i] + _sampleRight[i];
|
||||
}
|
||||
return _sampleResults;
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Subtraction")]
|
||||
public double[] DoubleSubtract()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_doubleResults[i] = _doubleLeft[i] - _doubleRight[i];
|
||||
}
|
||||
return _doubleResults;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Subtraction")]
|
||||
public DoubleDouble[] DoubleDoubleSubtract()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_results[i] = _left[i] - _right[i];
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Subtraction")]
|
||||
public DoubleDoubleSample[] DoubleDoubleSampleSubtract()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_sampleResults[i] = _sampleLeft[i] - _sampleRight[i];
|
||||
}
|
||||
return _sampleResults;
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Multiplication")]
|
||||
public double[] DoubleMultiply()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_doubleResults[i] = _doubleLeft[i] * _doubleRight[i];
|
||||
}
|
||||
return _doubleResults;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Multiplication")]
|
||||
public DoubleDouble[] DoubleDoubleMultiply()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_results[i] = _left[i] * _right[i];
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Multiplication")]
|
||||
public DoubleDoubleSample[] DoubleDoubleSampleMultiply()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_sampleResults[i] = _sampleLeft[i] * _sampleRight[i];
|
||||
}
|
||||
return _sampleResults;
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Division")]
|
||||
public double[] DoubleDivide()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_doubleResults[i] = _doubleLeft[i] / _doubleRight[i];
|
||||
}
|
||||
return _doubleResults;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Division")]
|
||||
public DoubleDouble[] DoubleDoubleDivide()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_results[i] = _left[i] / _right[i];
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Division")]
|
||||
public DoubleDoubleSample[] DoubleDoubleSampleDivide()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_sampleResults[i] = _sampleLeft[i] / _sampleRight[i];
|
||||
}
|
||||
return _sampleResults;
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,135 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using Just.PreciseMath.PerformanceComparison.Reference;
|
||||
|
||||
namespace Just.PreciseMath.PerformanceComparison;
|
||||
|
||||
/// <summary>
|
||||
/// Compares Square, Sqrt, and Abs on the same ordinary finite operands across binary64,
|
||||
/// the <c>Just.PreciseMath</c> double-double, and the blog reference implementation.
|
||||
/// Every category measures one function, and its binary64 method is the baseline, so
|
||||
/// the ratio columns read as "times the cost of a double".
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Operands are independent array elements, so the measurements include loads, stores,
|
||||
/// and loop overhead on purpose; input construction and fixture validation stay outside
|
||||
/// the timed methods.
|
||||
/// </remarks>
|
||||
[MemoryDiagnoser]
|
||||
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
|
||||
[CategoriesColumn]
|
||||
public class MathComparisonBenchmarks
|
||||
{
|
||||
private double[] _doublePositive = [];
|
||||
private DoubleDouble[] _positive = [];
|
||||
private DoubleDoubleSample[] _samplePositive = [];
|
||||
private double[] _doubleMixed = [];
|
||||
private DoubleDouble[] _mixed = [];
|
||||
private DoubleDoubleSample[] _sampleMixed = [];
|
||||
private readonly double[] _doubleResults = new double[PerformanceInputs.Count];
|
||||
private readonly DoubleDouble[] _results = new DoubleDouble[PerformanceInputs.Count];
|
||||
private readonly DoubleDoubleSample[] _sampleResults = new DoubleDoubleSample[PerformanceInputs.Count];
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
PerformanceInputs inputs = PerformanceInputs.Create();
|
||||
_doublePositive = inputs.DoublePositive;
|
||||
_positive = inputs.Positive;
|
||||
_samplePositive = inputs.SamplePositive;
|
||||
// Square and Sqrt need positive operands; Abs also covers negative ones.
|
||||
_doubleMixed = inputs.DoubleLeft;
|
||||
_mixed = inputs.Left;
|
||||
_sampleMixed = inputs.SampleLeft;
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Square")]
|
||||
public double[] DoubleSquare()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_doubleResults[i] = _doublePositive[i] * _doublePositive[i];
|
||||
}
|
||||
return _doubleResults;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Square")]
|
||||
public DoubleDouble[] DoubleDoubleSquare()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_results[i] = DoubleDouble.Square(_positive[i]);
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Square")]
|
||||
public DoubleDoubleSample[] DoubleDoubleSampleSquare()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_sampleResults[i] = _samplePositive[i].Square();
|
||||
}
|
||||
return _sampleResults;
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Sqrt")]
|
||||
public double[] DoubleSqrt()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_doubleResults[i] = Math.Sqrt(_doublePositive[i]);
|
||||
}
|
||||
return _doubleResults;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Sqrt")]
|
||||
public DoubleDouble[] DoubleDoubleSqrt()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_results[i] = DoubleDouble.Sqrt(_positive[i]);
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Sqrt")]
|
||||
public DoubleDoubleSample[] DoubleDoubleSampleSqrt()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_sampleResults[i] = _samplePositive[i].Sqrt();
|
||||
}
|
||||
return _sampleResults;
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Abs")]
|
||||
public double[] DoubleAbs()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_doubleResults[i] = Math.Abs(_doubleMixed[i]);
|
||||
}
|
||||
return _doubleResults;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Abs")]
|
||||
public DoubleDouble[] DoubleDoubleAbs()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_results[i] = DoubleDouble.Abs(_mixed[i]);
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = PerformanceInputs.Count), BenchmarkCategory("Abs")]
|
||||
public DoubleDoubleSample[] DoubleDoubleSampleAbs()
|
||||
{
|
||||
for (int i = 0; i < PerformanceInputs.Count; i++)
|
||||
{
|
||||
_sampleResults[i] = _sampleMixed[i].Abs();
|
||||
}
|
||||
return _sampleResults;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using Just.PreciseMath.PerformanceComparison.Reference;
|
||||
|
||||
namespace Just.PreciseMath.PerformanceComparison;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic operand sets for the implementation comparison: <see cref="Count"/>
|
||||
/// values per operation, each with a nonzero high and low component, built from exact
|
||||
/// dyadic multiples and held inside a moderate exponent range so that no benchmark
|
||||
/// path takes an overflow, underflow, or boundary branch.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Both double-double implementations receive bit-identical operands, and the plain
|
||||
/// doubles equal the high components, so the binary64 reference measures the leading
|
||||
/// component of the same values. The mixed-sign set feeds +, -, *, / and Abs; the
|
||||
/// nonnegative set feeds Square and Sqrt.
|
||||
/// </remarks>
|
||||
internal sealed class PerformanceInputs
|
||||
{
|
||||
/// <summary>Operands per benchmark invocation. One double-double operation takes only
|
||||
/// a few nanoseconds, so an invocation covers many values and the measurement is
|
||||
/// normalized back to a single operation.</summary>
|
||||
internal const int Count = 200;
|
||||
|
||||
/// <summary>Mixed-sign operands for +, -, *, / and Abs.</summary>
|
||||
internal DoubleDouble[] Left { get; } = new DoubleDouble[Count];
|
||||
|
||||
internal DoubleDouble[] Right { get; } = new DoubleDouble[Count];
|
||||
|
||||
/// <summary>Nonnegative operands for Square and Sqrt, with both components positive.</summary>
|
||||
internal DoubleDouble[] Positive { get; } = new DoubleDouble[Count];
|
||||
|
||||
/// <summary>The same operands for the blog reference implementation.</summary>
|
||||
internal DoubleDoubleSample[] SampleLeft { get; } = new DoubleDoubleSample[Count];
|
||||
|
||||
internal DoubleDoubleSample[] SampleRight { get; } = new DoubleDoubleSample[Count];
|
||||
|
||||
internal DoubleDoubleSample[] SamplePositive { get; } = new DoubleDoubleSample[Count];
|
||||
|
||||
/// <summary>The high components as plain doubles, for the binary64 reference.</summary>
|
||||
internal double[] DoubleLeft { get; } = new double[Count];
|
||||
|
||||
internal double[] DoubleRight { get; } = new double[Count];
|
||||
|
||||
internal double[] DoublePositive { get; } = new double[Count];
|
||||
|
||||
internal static PerformanceInputs Create()
|
||||
{
|
||||
PerformanceInputs inputs = new PerformanceInputs();
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
// Highs are small dyadic multiples of a power of two: exact in binary64,
|
||||
// with the significand needing at most nine bits. Lows are multiples of
|
||||
// 2^(exponent-57) or 2^(exponent-60), at least two bits below half an ulp
|
||||
// of their high, so every pair is normalized and both components survive
|
||||
// every operation. The exponents keep highs within 2^+-48, which leaves
|
||||
// products, quotients, and squares far from the binary64 exponent limits.
|
||||
int leftExponent = (((i / 4) % 5) - 2) * 24;
|
||||
int rightExponent = (((i / 4) % 3) - 1) * 16;
|
||||
int positiveExponent = ((i % 5) - 2) * 24;
|
||||
double leftSign = (i % 2 == 0) ? 1.0 : -1.0;
|
||||
double rightSign = (i % 3 == 0) ? 1.0 : -1.0;
|
||||
double leftHigh = leftSign * Math.ScaleB(1.25 + ((i % 7) / 32.0), leftExponent);
|
||||
double leftLow = leftSign * Math.ScaleB(1.0 + (i % 4), leftExponent - 57);
|
||||
double rightHigh = rightSign * Math.ScaleB(1.0 + (((i % 5) - 2) / 128.0), rightExponent);
|
||||
// Opposite sign, and a three-bit finer scale than the left low: no
|
||||
// addition or multiplication of the two lows can cancel to a value that
|
||||
// binary64 would represent without a residual.
|
||||
double rightLow = -rightSign * Math.ScaleB(1.0 + ((i / 2) % 3), rightExponent - 60);
|
||||
double positiveHigh = Math.ScaleB(1.25 + ((i % 7) / 32.0), positiveExponent);
|
||||
double positiveLow = Math.ScaleB(1.0 + (i % 4), positiveExponent - 57);
|
||||
|
||||
inputs.Left[i] = DoubleDouble.FromComponents(leftHigh, leftLow);
|
||||
inputs.Right[i] = DoubleDouble.FromComponents(rightHigh, rightLow);
|
||||
inputs.Positive[i] = DoubleDouble.FromComponents(positiveHigh, positiveLow);
|
||||
inputs.SampleLeft[i] = DoubleDoubleSample.FromTwoDoubles(leftHigh, leftLow);
|
||||
inputs.SampleRight[i] = DoubleDoubleSample.FromTwoDoubles(rightHigh, rightLow);
|
||||
inputs.SamplePositive[i] = DoubleDoubleSample.FromTwoDoubles(positiveHigh, positiveLow);
|
||||
inputs.DoubleLeft[i] = leftHigh;
|
||||
inputs.DoubleRight[i] = rightHigh;
|
||||
inputs.DoublePositive[i] = positiveHigh;
|
||||
}
|
||||
|
||||
inputs.Validate();
|
||||
return inputs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup-only fixture guard. It checks the operand shape and that every benchmarked
|
||||
/// operation consumes both components; it is not an accuracy oracle and does not
|
||||
/// compare the two implementations against each other.
|
||||
/// </summary>
|
||||
private void Validate()
|
||||
{
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
ValidateOperand(Left[i], nameof(Left), i);
|
||||
ValidateOperand(Right[i], nameof(Right), i);
|
||||
ValidateOperand(Positive[i], nameof(Positive), i);
|
||||
ValidateMatch(Left[i], SampleLeft[i], nameof(Left), i);
|
||||
ValidateMatch(Right[i], SampleRight[i], nameof(Right), i);
|
||||
ValidateMatch(Positive[i], SamplePositive[i], nameof(Positive), i);
|
||||
if (DoubleLeft[i] != Left[i].High || DoubleRight[i] != Right[i].High || DoublePositive[i] != Positive[i].High)
|
||||
{
|
||||
throw new InvalidOperationException($"[{i}] the binary64 operands do not match the leading components.");
|
||||
}
|
||||
if (!(Positive[i].High > 0.0) || !(Positive[i].Low > 0.0))
|
||||
{
|
||||
throw new InvalidOperationException($"[{i}] Square and Sqrt operands must be positive in both components.");
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
CheckResidual("Left + Right", Left[i] + Right[i], i);
|
||||
CheckResidual("Left - Right", Left[i] - Right[i], i);
|
||||
CheckResidual("Left * Right", Left[i] * Right[i], i);
|
||||
CheckResidual("Left / Right", Left[i] / Right[i], i);
|
||||
CheckResidual("Positive * Positive", DoubleDouble.Square(Positive[i]), i);
|
||||
CheckResidual("Sqrt(Positive)", DoubleDouble.Sqrt(Positive[i]), i);
|
||||
CheckResidual("Abs(Left)", DoubleDouble.Abs(Left[i]), i);
|
||||
CheckResidual("SampleLeft + SampleRight", SampleLeft[i] + SampleRight[i], i);
|
||||
CheckResidual("SampleLeft - SampleRight", SampleLeft[i] - SampleRight[i], i);
|
||||
CheckResidual("SampleLeft * SampleRight", SampleLeft[i] * SampleRight[i], i);
|
||||
CheckResidual("SampleLeft / SampleRight", SampleLeft[i] / SampleRight[i], i);
|
||||
CheckResidual("SamplePositive.Square()", SamplePositive[i].Square(), i);
|
||||
CheckResidual("SamplePositive.Sqrt()", SamplePositive[i].Sqrt(), i);
|
||||
CheckResidual("SampleLeft.Abs()", SampleLeft[i].Abs(), i);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateOperand(DoubleDouble value, string name, int index)
|
||||
{
|
||||
// FromComponents renormalizes an arbitrary pair, so reproducing the value
|
||||
// bit-for-bit is the normalization check for a purely dyadic construction.
|
||||
bool normalized = DoubleDouble.FromComponents(value.High, value.Low).Equals(value);
|
||||
if (!DoubleDouble.IsFinite(value) || value.High == 0.0 || value.Low == 0.0 || !normalized
|
||||
|| Math.Abs(Math.ILogB(value.High)) > 100)
|
||||
{
|
||||
throw new InvalidOperationException($"{name}[{index}] is not an ordinary normalized value with both components used.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateMatch(DoubleDouble value, DoubleDoubleSample sample, string name, int index)
|
||||
{
|
||||
bool normalized = DoubleDoubleSample.FromTwoDoubles(sample.Hi, sample.Lo).Equals(sample);
|
||||
if (sample.Hi != value.High || sample.Lo != value.Low || !normalized)
|
||||
{
|
||||
throw new InvalidOperationException($"{name}[{index}] differs between the two double-double implementations.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckResidual(string operation, DoubleDouble result, int index)
|
||||
{
|
||||
if (!DoubleDouble.IsFinite(result) || result.Low == 0.0)
|
||||
{
|
||||
throw new InvalidOperationException($"{operation}[{index}] left the ordinary range or lost its low component.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckResidual(string operation, DoubleDoubleSample result, int index)
|
||||
{
|
||||
if (!double.IsFinite(result.Hi) || result.Lo == 0.0)
|
||||
{
|
||||
throw new InvalidOperationException($"{operation}[{index}] left the ordinary range or lost its low component.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using BenchmarkDotNet.Reports;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
BenchmarkSwitcher switcher = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly);
|
||||
// Only these standalone discovery/help forms intentionally produce no reports.
|
||||
if (args is ["--list", "flat" or "tree"] or ["--help"] or ["-h"])
|
||||
{
|
||||
switcher.Run(args);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Summary[] summaries = [.. switcher.Run(args.Length == 0 ? ["--filter", "*"] : args)];
|
||||
// BenchmarkDotNet can return normally after validation, build, or execution
|
||||
// failures. Do not let CI mistake an empty or failed run for a passing smoke test.
|
||||
if (summaries.Length == 0 || summaries.Any(summary =>
|
||||
summary.HasCriticalValidationErrors || summary.Reports.Length == 0 ||
|
||||
summary.Reports.Any(report => !report.Success || report.ResultStatistics is null)))
|
||||
{
|
||||
Console.Error.WriteLine("Benchmark run failed or produced no measurements.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
// Printing and parsing of DoubleDoubleSample, by Marek Fiser.
|
||||
// Source: https://marekfiser.com/blog/double-double-arithmetic
|
||||
// Licensed under CC BY 4.0: https://creativecommons.org/licenses/by/4.0/
|
||||
// Changes for this benchmark: copied into this project, kept in the
|
||||
// Just.PreciseMath.PerformanceComparison.Reference namespace, and the file and type
|
||||
// renamed to DoubleDoubleSample.Format.cs and DoubleDoubleSample.
|
||||
// Printing 31 correct decimal digits requires computing them exactly, which is done with
|
||||
// BigInteger (part of the BCL, so still no NuGet dependencies).
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace Just.PreciseMath.PerformanceComparison.Reference;
|
||||
|
||||
public readonly partial struct DoubleDoubleSample {
|
||||
|
||||
/// <summary>Significant digits <see cref="ToString()"/> prints: enough to round-trip any
|
||||
/// value whose two components are contiguous (see <see cref="Parse"/>).</summary>
|
||||
private const int DEFAULT_DIGITS = 36;
|
||||
|
||||
|
||||
/// <summary>Formats with <see cref="DEFAULT_DIGITS"/> significant digits.</summary>
|
||||
public override string ToString() {
|
||||
return ToString(DEFAULT_DIGITS);
|
||||
}
|
||||
|
||||
/// <summary>Formats with the given number of significant decimal digits, correctly rounded.</summary>
|
||||
public string ToString(int significantDigits) {
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(significantDigits, 1);
|
||||
|
||||
if (!double.IsFinite(m_hi)) {
|
||||
return m_hi.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
(BigInteger numerator, BigInteger denominator) = toRational(this);
|
||||
return formatRational(numerator, denominator, significantDigits);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the exact value. A double-double is a sum of two binary fractions, so it
|
||||
/// always has a finite decimal expansion, but it can be hundreds of digits long when the
|
||||
/// components are far apart.
|
||||
/// </summary>
|
||||
public string ToStringExact() {
|
||||
if (!double.IsFinite(m_hi)) {
|
||||
return m_hi.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
(BigInteger numerator, BigInteger denominator) = toRational(this);
|
||||
if (numerator.IsZero) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
// The denominator is a power of two, so multiplying by the matching power of five
|
||||
// turns it into a power of ten, which gives the decimal digits directly.
|
||||
int twos = (int)(denominator.GetBitLength() - 1);
|
||||
BigInteger scaled = BigInteger.Abs(numerator) * BigInteger.Pow(5, twos);
|
||||
string digits = scaled.ToString(CultureInfo.InvariantCulture).PadLeft(twos + 1, '0');
|
||||
string sign = numerator.Sign < 0 ? "-" : "";
|
||||
if (twos == 0) {
|
||||
return sign + digits;
|
||||
}
|
||||
|
||||
string result = sign + digits[..^twos] + "." + digits[^twos..];
|
||||
return result.TrimEnd('0').TrimEnd('.');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a decimal string to the nearest double-double. The value is built exactly as a
|
||||
/// rational, then rounded once into each component. Round-trips with
|
||||
/// <see cref="ToString()"/> for every value whose components are contiguous;
|
||||
/// a hand-built pair such as (1.0, 1e-300) holds more information than 36 digits can
|
||||
/// represent.
|
||||
/// </summary>
|
||||
public static DoubleDoubleSample Parse(string text) {
|
||||
(BigInteger numerator, BigInteger denominator) = parseRational(text);
|
||||
double hi = toNearestDouble(numerator, denominator);
|
||||
if (!double.IsFinite(hi)) {
|
||||
return FromDouble(hi);
|
||||
}
|
||||
|
||||
// Subtract the leading component exactly and round the remainder into the trailing one.
|
||||
(BigInteger hiNumerator, BigInteger hiDenominator) = toRational(FromDouble(hi));
|
||||
BigInteger restNumerator = numerator * hiDenominator - hiNumerator * denominator;
|
||||
BigInteger restDenominator = denominator * hiDenominator;
|
||||
double lo = toNearestDouble(restNumerator, restDenominator);
|
||||
return FromTwoDoubles(hi, lo);
|
||||
}
|
||||
|
||||
/// <summary>The exact value of a double-double as a rational (the denominator is a power of two).</summary>
|
||||
private static (BigInteger Numerator, BigInteger Denominator) toRational(DoubleDoubleSample value) {
|
||||
(BigInteger hiNumerator, BigInteger hiDenominator) = toRational(value.m_hi);
|
||||
(BigInteger loNumerator, BigInteger loDenominator) = toRational(value.m_lo);
|
||||
return (hiNumerator * loDenominator + loNumerator * hiDenominator, hiDenominator * loDenominator);
|
||||
}
|
||||
|
||||
/// <summary>The exact value of a finite double as a rational, from its bits.</summary>
|
||||
private static (BigInteger Numerator, BigInteger Denominator) toRational(double value) {
|
||||
long bits = BitConverter.DoubleToInt64Bits(value);
|
||||
int exponent = (int)((bits >> 52) & 0x7FF);
|
||||
long mantissa = bits & 0xF_FFFF_FFFF_FFFFL;
|
||||
if (exponent == 0) {
|
||||
exponent = -1074; // subnormal: no implicit leading bit
|
||||
} else {
|
||||
mantissa |= 1L << 52;
|
||||
exponent -= 1075;
|
||||
}
|
||||
|
||||
BigInteger numerator = bits < 0 ? -mantissa : mantissa;
|
||||
return exponent >= 0
|
||||
? (numerator << exponent, BigInteger.One)
|
||||
: (numerator, BigInteger.One << -exponent);
|
||||
}
|
||||
|
||||
/// <summary>Rounds an exact rational to the nearest double, ties to even.</summary>
|
||||
private static double toNearestDouble(BigInteger numerator, BigInteger denominator) {
|
||||
if (numerator.IsZero) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
int sign = numerator.Sign;
|
||||
numerator = BigInteger.Abs(numerator);
|
||||
|
||||
// Scale so that the quotient is in [2^52, 2^54). One integer division then gives all
|
||||
// bits of the significand plus the remainder needed for the rounding.
|
||||
long exponent = numerator.GetBitLength() - denominator.GetBitLength() - 53;
|
||||
if (exponent > 0) {
|
||||
denominator <<= (int)exponent;
|
||||
} else {
|
||||
numerator <<= (int)-exponent;
|
||||
}
|
||||
|
||||
BigInteger quotient = BigInteger.DivRem(numerator, denominator, out BigInteger remainder);
|
||||
if (quotient.GetBitLength() > 53) {
|
||||
// One bit too many. Move it to the exponent and add the dropped bit to the
|
||||
// remainder so that the rounding decision below stays exact.
|
||||
if (!quotient.IsEven) {
|
||||
remainder += denominator;
|
||||
}
|
||||
quotient >>= 1;
|
||||
denominator <<= 1;
|
||||
exponent++;
|
||||
}
|
||||
|
||||
int comparison = (remainder << 1).CompareTo(denominator);
|
||||
if (comparison > 0 || (comparison == 0 && !quotient.IsEven)) {
|
||||
quotient++;
|
||||
if (quotient.GetBitLength() > 53) {
|
||||
quotient >>= 1;
|
||||
exponent++;
|
||||
}
|
||||
}
|
||||
|
||||
return sign * Math.ScaleB((double)quotient, (int)exponent);
|
||||
}
|
||||
|
||||
/// <summary>Parses a decimal string such as "-1.25e-7" into an exact rational.</summary>
|
||||
private static (BigInteger Numerator, BigInteger Denominator) parseRational(string text) {
|
||||
text = text.Trim();
|
||||
if (text.Length == 0) {
|
||||
throw new FormatException("Empty input.");
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
bool negative = text[index] == '-';
|
||||
if (negative || text[index] == '+') {
|
||||
index++;
|
||||
}
|
||||
|
||||
BigInteger mantissa = BigInteger.Zero;
|
||||
int fractionDigits = 0;
|
||||
bool seenDot = false;
|
||||
bool seenDigit = false;
|
||||
|
||||
for (; index < text.Length; index++) {
|
||||
char c = text[index];
|
||||
if (c == '.') {
|
||||
if (seenDot) {
|
||||
throw new FormatException($"Two decimal points in '{text}'.");
|
||||
}
|
||||
seenDot = true;
|
||||
} else if (c is >= '0' and <= '9') {
|
||||
mantissa = mantissa * 10 + (c - '0');
|
||||
seenDigit = true;
|
||||
if (seenDot) {
|
||||
fractionDigits++;
|
||||
}
|
||||
} else if (c is 'e' or 'E') {
|
||||
break;
|
||||
} else {
|
||||
throw new FormatException($"Unexpected character '{c}' in '{text}'.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!seenDigit) {
|
||||
throw new FormatException($"No digits in '{text}'.");
|
||||
}
|
||||
|
||||
int exponent = -fractionDigits;
|
||||
if (index < text.Length) {
|
||||
exponent += int.Parse(text[(index + 1)..], CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (negative) {
|
||||
mantissa = -mantissa;
|
||||
}
|
||||
|
||||
return exponent >= 0
|
||||
? (mantissa * BigInteger.Pow(10, exponent), BigInteger.One)
|
||||
: (mantissa, BigInteger.Pow(10, -exponent));
|
||||
}
|
||||
|
||||
/// <summary>Rounds an exact rational to N significant digits and formats them.</summary>
|
||||
private static string formatRational(BigInteger numerator, BigInteger denominator, int digits) {
|
||||
if (numerator.IsZero) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
bool negative = numerator.Sign < 0;
|
||||
numerator = BigInteger.Abs(numerator);
|
||||
|
||||
// Estimate of the decimal exponent of the leading digit from the bit lengths
|
||||
// (log10(2) ~ 0.30103). The loop below corrects it when the estimate is off by one.
|
||||
int exponent10 = (int)Math.Floor((numerator.GetBitLength() - denominator.GetBitLength()) * 0.30103);
|
||||
string text;
|
||||
|
||||
while (true) {
|
||||
int scale = digits - 1 - exponent10;
|
||||
BigInteger scaledNumerator = numerator;
|
||||
BigInteger scaledDenominator = denominator;
|
||||
if (scale >= 0) {
|
||||
scaledNumerator *= BigInteger.Pow(10, scale);
|
||||
} else {
|
||||
scaledDenominator *= BigInteger.Pow(10, -scale);
|
||||
}
|
||||
BigInteger rounded = roundToNearest(scaledNumerator, scaledDenominator);
|
||||
text = rounded.ToString(CultureInfo.InvariantCulture);
|
||||
if (text.Length == digits) {
|
||||
break;
|
||||
}
|
||||
// Off by one either way (estimate too low, or rounding carried into a new digit).
|
||||
exponent10 += text.Length - digits;
|
||||
}
|
||||
|
||||
text = text.TrimEnd('0');
|
||||
if (text.Length == 0) {
|
||||
text = "0";
|
||||
}
|
||||
|
||||
StringBuilder result = new(negative ? "-" : "");
|
||||
if (exponent10 >= -5 && exponent10 < digits) {
|
||||
// Positional notation for the range where it is readable.
|
||||
if (exponent10 >= 0) {
|
||||
text = text.PadRight(exponent10 + 1, '0');
|
||||
result.Append(text[..(exponent10 + 1)]);
|
||||
if (text.Length > exponent10 + 1) {
|
||||
result.Append('.').Append(text[(exponent10 + 1)..]);
|
||||
}
|
||||
} else {
|
||||
result.Append("0.").Append('0', -exponent10 - 1).Append(text);
|
||||
}
|
||||
} else {
|
||||
result.Append(text[0]);
|
||||
if (text.Length > 1) {
|
||||
result.Append('.').Append(text[1..]);
|
||||
}
|
||||
result.Append('e').Append(exponent10.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>Integer nearest to numerator/denominator, ties away from zero (both positive).</summary>
|
||||
private static BigInteger roundToNearest(BigInteger numerator, BigInteger denominator) {
|
||||
BigInteger quotient = BigInteger.DivRem(numerator, denominator, out BigInteger remainder);
|
||||
return (remainder << 1) >= denominator ? quotient + 1 : quotient;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// Double-double arithmetic in about 200 lines, by Marek Fiser.
|
||||
// Source: https://marekfiser.com/blog/double-double-arithmetic
|
||||
// Licensed under CC BY 4.0: https://creativecommons.org/licenses/by/4.0/
|
||||
// Changes for this benchmark: copied into this project, kept in the
|
||||
// Just.PreciseMath.PerformanceComparison.Reference namespace, and the file and type
|
||||
// renamed to DoubleDoubleSample.
|
||||
// This file depends only on System so that it can be copied into any project. Printing and
|
||||
// parsing use BigInteger and are in DoubleDoubleSample.Format.cs.
|
||||
namespace Just.PreciseMath.PerformanceComparison.Reference;
|
||||
|
||||
/// <summary>
|
||||
/// A value stored as the unevaluated sum of two doubles: ~106 significand bits
|
||||
/// (~31 decimal digits) at a few times the cost of a double, with no heap allocation.
|
||||
///
|
||||
/// Invariant: Hi == RoundToNearest(Hi + Lo), i.e. |Lo| is at most half an ulp of Hi.
|
||||
/// Every factory and operation below maintains it; comparisons and ToDouble rely on it.
|
||||
///
|
||||
/// Operations are not correctly rounded but stay within a few units of 2^-106 of the
|
||||
/// exact result. The exponent range is the same as a plain double's, and infinities are
|
||||
/// not special-cased: the first operation on one returns NaN (the error terms compute
|
||||
/// inf - inf).
|
||||
///
|
||||
/// By Marek Fiser, from marekfiser.com/blog/double-double-arithmetic/ (CC BY 4.0; the file
|
||||
/// header carries the full attribution and license notice).
|
||||
/// </summary>
|
||||
public readonly partial struct DoubleDoubleSample : IEquatable<DoubleDoubleSample>, IComparable<DoubleDoubleSample> {
|
||||
|
||||
private readonly double m_hi;
|
||||
private readonly double m_lo;
|
||||
|
||||
/// <summary>The leading component; equals the value rounded to nearest double.</summary>
|
||||
public double Hi => m_hi;
|
||||
|
||||
/// <summary>The trailing component (the rounding error of <see cref="Hi"/>).</summary>
|
||||
public double Lo => m_lo;
|
||||
|
||||
public bool IsZero => m_hi == 0.0;
|
||||
|
||||
/// <summary>-1, 0 or +1 (0 also for NaN).</summary>
|
||||
public int Sign => m_hi > 0.0 ? 1 : m_hi < 0.0 ? -1 : 0;
|
||||
|
||||
public static DoubleDoubleSample Zero { get; } = new DoubleDoubleSample(0.0, 0.0);
|
||||
|
||||
public static DoubleDoubleSample One { get; } = new DoubleDoubleSample(1.0, 0.0);
|
||||
|
||||
|
||||
private DoubleDoubleSample(double hi, double lo) {
|
||||
m_hi = hi;
|
||||
m_lo = lo;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Exact conversion from a double.</summary>
|
||||
public static DoubleDoubleSample FromDouble(double value) {
|
||||
return new DoubleDoubleSample(value, 0.0);
|
||||
}
|
||||
|
||||
/// <summary>The exact sum <paramref name="hi"/> + <paramref name="lo"/> of two arbitrary doubles.</summary>
|
||||
public static DoubleDoubleSample FromTwoDoubles(double hi, double lo) {
|
||||
// Renormalizes so that the invariant holds.
|
||||
(double sum, double error) = twoSum(hi, lo);
|
||||
return new DoubleDoubleSample(sum, error);
|
||||
}
|
||||
|
||||
public DoubleDoubleSample Add(DoubleDoubleSample right) {
|
||||
// Add the two components pairwise, then incorporate lowSum and lowError one by one.
|
||||
// Adding lowError can round, so a second quickTwoSum renormalizes the result.
|
||||
(double sum, double error) = twoSum(m_hi, right.m_hi);
|
||||
(double lowSum, double lowError) = twoSum(m_lo, right.m_lo);
|
||||
error += lowSum;
|
||||
(sum, error) = quickTwoSum(sum, error);
|
||||
error += lowError;
|
||||
(sum, error) = quickTwoSum(sum, error);
|
||||
return new DoubleDoubleSample(sum, error);
|
||||
}
|
||||
|
||||
public DoubleDoubleSample Subtract(DoubleDoubleSample right) {
|
||||
return Add(right.Negate());
|
||||
}
|
||||
|
||||
public DoubleDoubleSample Multiply(DoubleDoubleSample right) {
|
||||
// hi*hi exactly, then the three cross terms. Each cross term is at least 2^-53 smaller
|
||||
// than the product, so plain double arithmetic is accurate enough for them.
|
||||
(double product, double error) = twoProduct(m_hi, right.m_hi);
|
||||
error += m_hi * right.m_lo + m_lo * right.m_hi + m_lo * right.m_lo;
|
||||
(product, error) = quickTwoSum(product, error);
|
||||
return new DoubleDoubleSample(product, error);
|
||||
}
|
||||
|
||||
/// <summary>Multiplies by a plain double. Faster than the full double-double product.</summary>
|
||||
public DoubleDoubleSample Multiply(double right) {
|
||||
(double product, double error) = twoProduct(m_hi, right);
|
||||
error += m_lo * right;
|
||||
(product, error) = quickTwoSum(product, error);
|
||||
return new DoubleDoubleSample(product, error);
|
||||
}
|
||||
|
||||
/// <summary>Slower and slightly less accurate than the other operations.</summary>
|
||||
public DoubleDoubleSample Divide(DoubleDoubleSample right) {
|
||||
// Three steps of long division.
|
||||
double quotient1 = m_hi / right.m_hi;
|
||||
DoubleDoubleSample remainder = Subtract(right.Multiply(quotient1));
|
||||
double quotient2 = remainder.m_hi / right.m_hi;
|
||||
remainder = remainder.Subtract(right.Multiply(quotient2));
|
||||
double quotient3 = remainder.m_hi / right.m_hi;
|
||||
|
||||
(double sum, double error) = quickTwoSum(quotient1, quotient2);
|
||||
return new DoubleDoubleSample(sum, error).Add(FromDouble(quotient3));
|
||||
}
|
||||
|
||||
/// <summary>Slightly faster than the general product.</summary>
|
||||
public DoubleDoubleSample Square() {
|
||||
(double product, double error) = twoProduct(m_hi, m_hi);
|
||||
error += 2.0 * m_hi * m_lo + m_lo * m_lo;
|
||||
(product, error) = quickTwoSum(product, error);
|
||||
return new DoubleDoubleSample(product, error);
|
||||
}
|
||||
|
||||
public DoubleDoubleSample Negate() {
|
||||
// Negation never rounds, so this is exact.
|
||||
return new DoubleDoubleSample(-m_hi, -m_lo);
|
||||
}
|
||||
|
||||
public DoubleDoubleSample Abs() {
|
||||
return Sign < 0 ? Negate() : this;
|
||||
}
|
||||
|
||||
/// <summary>Slightly less accurate than the other operations. Negative and NaN inputs
|
||||
/// return NaN, as Math.Sqrt does.</summary>
|
||||
public DoubleDoubleSample Sqrt() {
|
||||
if (!(m_hi > 0.0)) { // Not "m_hi <= 0.0", because this form is also true for NaN.
|
||||
return FromDouble(Math.Sqrt(m_hi));
|
||||
}
|
||||
// One Newton step from the double square root.
|
||||
double approx = Math.Sqrt(m_hi);
|
||||
DoubleDoubleSample residual = Subtract(FromDouble(approx).Square());
|
||||
double correction = residual.m_hi / (approx + approx);
|
||||
(double hi, double lo) = quickTwoSum(approx, correction);
|
||||
return new DoubleDoubleSample(hi, lo);
|
||||
}
|
||||
|
||||
public double ToDouble() {
|
||||
return m_hi;
|
||||
}
|
||||
|
||||
public int CompareTo(DoubleDoubleSample other) {
|
||||
// Correct only for normalized pairs.
|
||||
int hiComparison = m_hi.CompareTo(other.m_hi);
|
||||
return hiComparison != 0 ? hiComparison : m_lo.CompareTo(other.m_lo);
|
||||
}
|
||||
|
||||
public bool Equals(DoubleDoubleSample other) {
|
||||
return m_hi == other.m_hi && m_lo == other.m_lo;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) {
|
||||
return obj is DoubleDoubleSample other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode() {
|
||||
return HashCode.Combine(m_hi, m_lo);
|
||||
}
|
||||
|
||||
public static DoubleDoubleSample operator +(DoubleDoubleSample left, DoubleDoubleSample right) => left.Add(right);
|
||||
public static DoubleDoubleSample operator -(DoubleDoubleSample left, DoubleDoubleSample right) => left.Subtract(right);
|
||||
public static DoubleDoubleSample operator *(DoubleDoubleSample left, DoubleDoubleSample right) => left.Multiply(right);
|
||||
public static DoubleDoubleSample operator *(DoubleDoubleSample left, double right) => left.Multiply(right);
|
||||
public static DoubleDoubleSample operator /(DoubleDoubleSample left, DoubleDoubleSample right) => left.Divide(right);
|
||||
public static DoubleDoubleSample operator -(DoubleDoubleSample value) => value.Negate();
|
||||
|
||||
public static bool operator ==(DoubleDoubleSample left, DoubleDoubleSample right) => left.Equals(right);
|
||||
public static bool operator !=(DoubleDoubleSample left, DoubleDoubleSample right) => !left.Equals(right);
|
||||
public static bool operator <(DoubleDoubleSample left, DoubleDoubleSample right) => left.CompareTo(right) < 0;
|
||||
public static bool operator >(DoubleDoubleSample left, DoubleDoubleSample right) => left.CompareTo(right) > 0;
|
||||
public static bool operator <=(DoubleDoubleSample left, DoubleDoubleSample right) => left.CompareTo(right) <= 0;
|
||||
public static bool operator >=(DoubleDoubleSample left, DoubleDoubleSample right) => left.CompareTo(right) >= 0;
|
||||
|
||||
// Conversion from a double is exact, so it is implicit. Conversion to a double drops the
|
||||
// low component, so it is explicit.
|
||||
public static implicit operator DoubleDoubleSample(double value) => FromDouble(value);
|
||||
public static explicit operator double(DoubleDoubleSample value) => value.ToDouble();
|
||||
|
||||
/// <summary>Knuth's two-sum: sum + error == a + b exactly, for any two doubles.</summary>
|
||||
private static (double Sum, double Error) twoSum(double a, double b) {
|
||||
// The rounding error of a floating-point addition is itself representable as a double.
|
||||
// The intermediate results below may round, but their rounding errors cancel out.
|
||||
double sum = a + b;
|
||||
double bKept = sum - a; // The part of b that made it into the sum.
|
||||
double aKept = sum - bKept; // The part of a that made it into the sum.
|
||||
double aLost = a - aKept;
|
||||
double bLost = b - bKept;
|
||||
return (sum, aLost + bLost);
|
||||
}
|
||||
|
||||
/// <summary>Dekker's fast two-sum: the same guarantee as twoSum, valid only when
|
||||
/// |a| >= |b| (or a == 0).</summary>
|
||||
private static (double Sum, double Error) quickTwoSum(double a, double b) {
|
||||
double sum = a + b;
|
||||
double bKept = sum - a;
|
||||
return (sum, b - bKept); // a lost nothing, so only b's loss remains.
|
||||
}
|
||||
|
||||
/// <summary>Exact product: product + error == a * b exactly, as long as the product is a
|
||||
/// normal double. Below about 2e-292 the error no longer fits a double.</summary>
|
||||
private static (double Product, double Error) twoProduct(double a, double b) {
|
||||
// The fused multiply-add computes a*b - product with a single rounding, which is the
|
||||
// low half of the 106-bit product. On a CPU without an FMA instruction,
|
||||
// Math.FusedMultiplyAdd computes the same result in software, much slower.
|
||||
double product = a * b;
|
||||
double error = Math.FusedMultiplyAdd(a, b, -product);
|
||||
return (product, error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"BenchmarkDotNet": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.15.8, )",
|
||||
"resolved": "0.15.8",
|
||||
"contentHash": "paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==",
|
||||
"dependencies": {
|
||||
"BenchmarkDotNet.Annotations": "0.15.8",
|
||||
"CommandLineParser": "2.9.1",
|
||||
"Gee.External.Capstone": "2.3.0",
|
||||
"Iced": "1.21.0",
|
||||
"Microsoft.CodeAnalysis.CSharp": "4.14.0",
|
||||
"Microsoft.Diagnostics.Runtime": "3.1.512801",
|
||||
"Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.21",
|
||||
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||
"Perfolizer": "[0.6.1]",
|
||||
"System.Management": "9.0.5"
|
||||
}
|
||||
},
|
||||
"BenchmarkDotNet.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.15.8",
|
||||
"contentHash": "hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA=="
|
||||
},
|
||||
"CommandLineParser": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.1",
|
||||
"contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA=="
|
||||
},
|
||||
"Gee.External.Capstone": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.3.0",
|
||||
"contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw=="
|
||||
},
|
||||
"Iced": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.21.0",
|
||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||
},
|
||||
"Microsoft.CodeAnalysis.Analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.11.0",
|
||||
"contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg=="
|
||||
},
|
||||
"Microsoft.CodeAnalysis.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.14.0",
|
||||
"contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeAnalysis.Analyzers": "3.11.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.CodeAnalysis.CSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.14.0",
|
||||
"contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeAnalysis.Analyzers": "3.11.0",
|
||||
"Microsoft.CodeAnalysis.Common": "[4.14.0]"
|
||||
}
|
||||
},
|
||||
"Microsoft.Diagnostics.NETCore.Client": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.2.510501",
|
||||
"contentHash": "juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Diagnostics.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.512801",
|
||||
"contentHash": "0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Diagnostics.NETCore.Client": "0.2.410101"
|
||||
}
|
||||
},
|
||||
"Microsoft.Diagnostics.Tracing.TraceEvent": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.21",
|
||||
"contentHash": "/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.Diagnostics.NETCore.Client": "0.2.510501",
|
||||
"System.Reflection.TypeExtensions": "4.7.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg=="
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "6.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
|
||||
"Microsoft.Extensions.Options": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA=="
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
|
||||
"Microsoft.Extensions.Primitives": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ=="
|
||||
},
|
||||
"Perfolizer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.6.1",
|
||||
"contentHash": "CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==",
|
||||
"dependencies": {
|
||||
"Pragmastat": "3.2.4"
|
||||
}
|
||||
},
|
||||
"Pragmastat": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.2.4",
|
||||
"contentHash": "I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A=="
|
||||
},
|
||||
"System.CodeDom": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.5",
|
||||
"contentHash": "cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ=="
|
||||
},
|
||||
"System.Management": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.5",
|
||||
"contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==",
|
||||
"dependencies": {
|
||||
"System.CodeDom": "9.0.5"
|
||||
}
|
||||
},
|
||||
"System.Reflection.TypeExtensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.7.0",
|
||||
"contentHash": "VybpaOQQhqE6siHppMktjfGBw1GCwvCqiufqmP8F1nj7fTUNtW35LOEt3UZTEsECfo+ELAl/9o9nJx3U91i7vA=="
|
||||
},
|
||||
"just.precisemath": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user