logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·31.10.26
← Back to news

Double-double: 31 digits of precision without leaving the FPU

Original article
marekfiser.com|30 points|7 comments|by iliketrains|Aug 16, 2026

Double-double: 31 digits of precision without leaving the FPU

Posted 16 Aug 2026 by Marek Fiser | ⏱️ 26 min read

If you find that the standard 15 decimal digits provided by a double are insufficient, there is a clever workaround: combine two doubles and treat them as a single numerical entity.

This technique grants you approximately 31 decimal digits of precision. In a production kernel, this costs roughly 9×9\times9× the performance of a standard double (though isolated operations may range from 4×4\times4× to 12×12\times12×).

Because it requires zero no heap allocation and has no external dependencies, it sits comfortably in the performance sweet spot between a standard double and a full-blown arbitrary-precision library.

This article delves into the error-free transformations that enable this, compares the performance against MPFR, and identifies the limits of the approach.


The Precision Gap

Floating-point types generally fall into a few categories:

TypePrecision (Approx.)Performance Cost
float~7 digitsVirtually free
double~15 digitsVirtually free
Double-Double~31 digitsModerate (~9x)
Arbitrary (MPFR)UnlimitedHigh (81x+)

There is a glaring void between "not enough precision" and "orders of magnitude slower." I discovered this gap while zooming deep into the Mandelbrot set.

Mandelbrot Comparison Figure 1: A deep zoom comparison. The left side shows "blocky" artifacts where double precision fails; the right side remains sharp using double-double.

The Mandelbrot set is a fractal characterized by infinite spirals and self-similarity. Eventually, when zooming in, the distance between two adjacent pixels becomes smaller than the smallest representable difference between two double values. At this point, multiple pixels are rounded to the same coordinate, and the image transforms from a fractal into a visualization of the number format's limitations rounding errors.

As the magnitude of a number increases, the gap between representable doubles grows. Consequently:

  • Real axis issues: Appear as wide blocks (e.g., re=−0.74,im=0.13re = -0.74, im = 0.13re=−0.74,im=0.13).
  • Imaginary axis issues: Appear as tall blocks (e.g., re=−0.11,im=0.92re = -0.11, im = 0.92re=−0.11,im=0.92).

Why not just use MPFR or GMP?

The standard advice for needing more precision is to use an arbitrary-precision library like GMP or MPFR. While correct for open-ended precision needs, the overhead is significant:

  • Memory Overhead: Every value is stored as a heap block.
  • Allocation Pain: A value is essentially a pointer to a "limb" array; creating one triggers an allocation. Many wrappers allocate a new object for every single operation.
  • Computational Cost: Addition and multiplication require iterating through limb arrays, involving loads, stores, and data-dependent branching.
  • Deployment Friction: Since these are native C libraries, you must deal with P/Invoke, shipping platform-specific binaries, and data marshalling.
  • Legal Constraints: GMP and MPFR are LGPL. For some companies or closed-platform projects, static linking makes this a non-starter.

The Performance Hierarchy

My benchmarks revealed a very structured performance gap. In a real-world kernel:

  1. double →\rightarrow→ Baseline
  2. double-double →\rightarrow→ ≈9×\approx 9\times≈9× slower than double
  3. MPFR (at 31 digits) →\rightarrow→ ≈9×\approx 9\times≈9× slower than double-double (≈81×\approx 81\times≈81× total)

If MPFR is configured to allocate a new result for every operation, the gap between double-double and MPFR widens to nearly 40×40\times40×.


The Double-Double Concept

A floating-point type maintains a constant number of significant digits regardless of the number's scale; the exponent simply shifts the decimal point.

Consider these two values:

  1. A=111,222,333,444A = 111,222,333,444A=111,222,333,444
  2. B=0.555,666,777,888B = 0.555,666,777,888B=0.555,666,777,888

Both have 12 significant digits and fit in a double. However, their exact sum—111,222,333,444.555,666,777,888111,222,333,444.555,666,777,888111,222,333,444.555,666,777,888—requires 24 digits. A standard double (limited to ~15 digits) would round this to 111,222,333,444.555,66111,222,333,444.555,66111,222,333,444.555,66.

The Trick: If we store AAA and BBB side-by-side, we can treat them as one number. One double handles the most significant digits, and the other captures the remainder.

Definition: A double-double value is the unevaluated sum of two doubles: value=xhi+xlo\text{value} = x_{hi} + x_{lo}value=xhi​+xlo​ Invariant: xhix_{hi}xhi​ must be exactly what (xhi+xlo)(x_{hi} + x_{lo})(xhi​+xlo​) rounds to when evaluated as a single double.

The xhix_{hi}xhi​ component carries the primary value, while xlox_{lo}xlo​ stores the rounding error. Every subsequent operation is designed to maintain this invariant.

Implementation

Here is the basic structure of the type:

public readonly partial struct DoubleDouble : IEquatable<DoubleDouble>, IComparable<DoubleDouble> 
{ 
    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 Hi).
    /// </summary>
    public double Lo => m_lo; 
}

Double-Double vs. Quadruple Precision (binary128)

It is important to distinguish this from the IEEE 754 binary128 (quad-precision) format:

  • binary128: Uses a single 113-bit significand and a 15-bit exponent (reaching ≈104932\approx 10^{4932}≈104932).
  • Double-Double: Uses two 53-bit significands. This results in ≈106\approx 106≈106 combined significand bits but retains the standard double exponent range (overflowing at ≈10308\approx 10^{308}≈10308).

Error-Free Transformations

The entire system relies on a single fundamental insight: When a double-precision operation rounds, the discarded amount can be recovered as another double.

  • Understand that rounding is deterministic.
  • Capture the difference between the exact result and the rounded result.
  • Store that difference in xlox_{lo}xlo​.

When you add two... (Article continues into the mathematics of EFTs)