// 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; /// /// 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). /// public readonly partial struct DoubleDoubleSample : IEquatable, IComparable { private readonly double m_hi; private readonly double m_lo; /// The leading component; equals the value rounded to nearest double. public double Hi => m_hi; /// The trailing component (the rounding error of ). public double Lo => m_lo; public bool IsZero => m_hi == 0.0; /// -1, 0 or +1 (0 also for NaN). 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; } /// Exact conversion from a double. public static DoubleDoubleSample FromDouble(double value) { return new DoubleDoubleSample(value, 0.0); } /// The exact sum + of two arbitrary doubles. 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); } /// Multiplies by a plain double. Faster than the full double-double product. 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); } /// Slower and slightly less accurate than the other operations. 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)); } /// Slightly faster than the general product. 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; } /// Slightly less accurate than the other operations. Negative and NaN inputs /// return NaN, as Math.Sqrt does. 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(); /// Knuth's two-sum: sum + error == a + b exactly, for any two doubles. 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); } /// Dekker's fast two-sum: the same guarantee as twoSum, valid only when /// |a| >= |b| (or a == 0). 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. } /// 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. 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); } }