Decimal digits of π in O(√n) memory: Gourdon’s unpublished 2003 Theorem 2, reconstructed

In 2003 Xavier Gourdon announced that the n-th decimal digit of π could be found with an adjustable memory budget m instead of the usual O(log² n) — but never published how. This site reconstructs a concrete algorithm that hits his bound, implements it in Rust, and races it against the classical method live in your browser, on the same code the CLI uses.

Live demo

Race: Theorem 1 vs Theorem 2

Both lanes compute the same 10 digits, starting from the same position, using the same WebAssembly build of the code the CLI uses — one running Theorem 1 (the classical, O(log² n)-memory method), the other Theorem 2 (this reconstruction). Positions up to 200,000 are checked live against a shipped reference file.

Positions up to 2,000 are computed by the same exact fallback (pi_ref, an integer Machin sum) in both lanes, not by either theorem, so a race below 2,000 (e.g. the 1,000 preset) times identical code twice; it's kept as a preset because it's a fast, always-available sanity check, but a race above 2,000 is the one that actually compares the two theorems.

Position

Theorem 1

Elapsed: –

Time: – · Memory: –

Theorem 2

Elapsed: –

Time: – · Memory: –

Stream

Computes successive 10-digit blocks from scratch, one after another, using Theorem 1, never holding earlier digits in memory — the “keep churning” case that started this whole project.

The first ~200 blocks (positions up to 2,000) are computed by the same exact pi_ref fallback every other small position uses, not by Theorem 1's Gourdon method proper; the stream starts at 10,001 by default so it opens on Theorem 1 itself.

Start position

Memory: –

Background

The BBP formula (Bailey–Borwein–Plouffe, 1995) can produce hexadecimal (or any power-of-two-base) digits of π starting at an arbitrary position, without computing the digits before it, in roughly linear time and logarithmic memory. Nothing structurally similar is known in base 10 — decimal digits are hard for the same reason a base-10 BBP formula for π appears not to exist (see the postscript). Getting a decimal digit therefore means a different kind of algorithm: an accelerated series for π itself, truncated and manipulated so that only the digits you want fall out.

The timeline for that approach: Plouffe (1996) found a way to extract a decimal digit without storing every digit before it, at roughly cubic time. Bellard (1997) brought that down to O(n²)-ish time with O(log n) memory, no time–memory trade-off. Gourdon (2003) went further: his Theorem 1 gets a decimal digit in O(n² loglog n / log² n) time with O(log² n) memory — still effectively fixed, tiny memory, and it is the method this site's own CLI implements as its non-default method (--method thm1). His Theorem 2 is the subject of this page: the same digit, but with a memory budget m you can dial up to trade for a large speed gain. He stated it. He did not publish how.

The claim

From Gourdon's 2003 preprint (Proposition 1 gives the working formula; the paragraph on intermediate memory sizes states Theorem 2 without proof):

With a memory budget of m bits (nε ≤ m ≤ n), the n-th decimal digit of π can be computed with an O(m)-bit working set, in roughly \(O\!\left(\dfrac{n^2 \log^3 n \operatorname{loglog} n}{m \log^2(n/m)}\right)\) time. “Details of this algorithm will [be] added soon.”

That follow-up never appeared. The PDF at Gourdon's own URL is unchanged since 12 Feb 2003 (same 9 pages, same creation date); the web version's TeX source has no commented-out details either. We found no paper, thesis, program or forum post anywhere that gives Theorem 2's algorithm, or even cites it with any detail beyond the one-paragraph statement above.

What the literature has

Sources checked for any trace of Gourdon's Theorem 2
SourceWhat it contains
X. Gourdon, Computation of the n-th decimal digit of π with low memory, 11 Feb 2003. PDF: numbers.computation.free.fr (mirror: plouffe.fr, Scribd copy) Theorem 1 in full; Theorem 2 stated only. Still dated 12 Feb 2003 (PDF CreationDate), 9 pages, same text as ever. No later version exists at that URL.
nthdigit.html and its TeX source nthdigit.tex Web version of the same text. “Generalization for intermediate memory size” repeats the bound and says “Details of this algorithm will added soon in [3]” ([3] = the 2003 preprint). The TeX has no commented-out details. Generated by TTH on 12 Feb 2003.
pidec (Gourdon's program, linked from the page above) Theorem 1 only (per the paper's own timing section).
github.com/logc/pidec Racket digit extractor “based on Fabrice Bellard's paper.” Not Gourdon's Theorem 2.
F. Bellard, pi_n2.html, pi.c/pi1.c O(n²)-ish, O(log n)-memory decimal digit extraction. No trade-off.
S. Plouffe, arXiv:0912.0303 and arXiv:2201.12601 Low-memory / formula-based n-th digit. No time–memory trade-off, no Theorem 2 details.
Cheng, Hanrot, Thomé, Zima, Zimmermann, Time- and space-efficient evaluation of some hypergeometric constants, arXiv:cs/0701151 Binary splitting with O(d) space for all first d digits. Doesn't cite Gourdon and does no digit extraction. Relevant only as the “m ≈ n” end of the trade-off.
Gourdon–Sebah truncation trick / Yakhontov, arXiv:1209.5097 O(p)-space binary splitting for all digits. Same remark.

As far as we can tell, the reconstruction below is the only written version of Theorem 2's algorithm anywhere.

The reconstruction

Everything below is our own work: an algorithm we believe matches Gourdon's stated bound and is plausibly close to what he had in mind, not a recovery of his actual method (which remains unpublished). Claims are tagged proved (argued in full, either here or in the fuller write-up), measured (prototype or benchmark data), or conjecture (stated as a belief, not a proof).

The accelerated series

Start from Gourdon's Theorem 1 identity, a Cohen–Villegas–Zagier-accelerated form of π/4 = Σ(−1)k/(2k+1):

\[ S = \sum_{k=0}^{(M+1)N-1} (-1)^k \frac{4}{2k+1} \;-\; \sum_{k=0}^{N-1} (-1)^k \frac{4\,s_k}{2^N(2MN+2k+1)}, \qquad s_k = \sum_{j=0}^{k} \binom{N}{j}, \qquad |S-\pi| \le \frac{\pi}{(2eM)^N}. \]

Choosing M and N (both even, N ≤ n+2) makes every quantity in sight an ordinary machine-word modular fraction: Gourdon's Proposition 1 turns this into frac(10nπ) ≈ frac(B − C) with error < 10−n₀,

\[ B = \sum_{k<(M+1)N} (-1)^k \frac{4\cdot 10^n \bmod (2k{+}1)}{2k+1}, \qquad C = \sum_{k<N} (-1)^k \frac{X\cdot s_k \bmod m_k}{m_k}, \]

with X = 5N−2·10n−N+2 and mk = 2MN+2k+1. Theorem 1 takes M ≈ 2n/log³n, so B is cheap and C costs O(k) word operations per sk — that O(k) inner sum is exactly the cost Theorem 2 needs to beat.

Why “blocking gains nothing” doesn't hold proved

The obvious objection to any memory-budget scheme: update an m-bit accumulator one small factor at a time, and it costs O(m/w) work per factor (w = word size) — no better than doing it in full precision. That's true for naive blocking. It's false for binary splitting: the factors (N−j+1) and j in the recurrence below are small integers that don't depend on the block modulus, so g ≈ m/log n of them can be multiplied together exactly with a product tree, costing O(M(m) log m) total — polylog per factor instead of m/w — then reduced once mod the block modulus Q. With fast (subquadratic) multiplication the cost per (term, j) pair drops from Θ(1) to Θ(log n·log²m/m). With schoolbook multiplication the gain collapses to a constant, so the real requirement is subquadratic multiplication, not a cleverer accumulator.

The (P, T, D) recurrence

For target t, define state Pt = N!/(N−t)!, Dt = t!, Tt = t!·st, with (P₀,T₀,D₀)=(1,1,1). One step (1 ≤ j) is P ← (N−j+1)P; T ← jT+(N−j+1)Pold; D ← jD. A block of steps composes as a triple (α,δ,τ) acting on (P,T,D), and triples compose by standard binary splitting: (α₁α₂, δ₁δ₂, δ₂τ₁+τ₂α₁). Then st ≡ TtDt−1 and C(N,t) ≡ PtDt−1 mod q, whenever gcd(q, t!) = 1. The symmetry sk = 2N − sN−1−k halves the targets that ever need computing.

Partial fractions: main, Lucas, p-adic

The obstacle Gourdon's obstacles list raises is real: mk can be composite and share prime factors with t! (Dt stops being invertible mod mk). The fix is to factor mk = Πpe and use partial fractions, frac(Xs/m) = Σq frac(Xs·((m/q)−1 mod q)/q), handling each coprime part q by whichever of three methods is valid for it:

PartConditionMethod
mainproduct of all pe with p > t the accumulating-remainder-tree item above (target t, modulus q) — Dt is invertible here by construction
Lucasp ≤ t, p ∥ mk Lucas' theorem, reducing to one small residue mod p (below)
p-adicp ≤ t, pe ∥ mk, e ≥ 2 the p-adic Lucas recursion (below), no big numbers
Lucas' theorem, applied proved

Write digits N = ΣNipi and t = Σtipi, with Gi(x) = Σy<xC(Ni,y) mod p:

\[ s_t \equiv \sum_i \Big[\!\prod_{u>i} \binom{N_u}{t_u}\Big]\, G_i(t_i)\, 2^{\sum_{u<i} N_u} \;+\; \prod_u \binom{N_u}{t_u} \pmod{p}. \]

For i = 0, C(N₀,y) ≡ C(N,y) for y < p, so G₀(t₀) and C(N₀,t₀) both come straight from the main-item computation above. For i ≥ 1, Ni ≤ N/pi, so those rows cost O(min(p, N/p)) per prime — this is Lucas' theorem plus a lexicographic split of {j ≤ t}.

The p-adic Lucas recursion (p² case) proved

Lucas' theorem alone breaks when p2 ∣ mk: a single residue mod p isn't enough. Write N = pK+N₀ and g(x) = ((1+x)p−1−xp)/p ∈ ℤ[x]. Then

\[ (1+x)^N \equiv \sum_{i<e} \binom{K}{i} p^i\, (1+x)^{N_0} g(x)^i (1+x^p)^{K-i} \pmod{p^e}. \]

Putting hi = (1+x)N₀gi (degree < (i+1)p, with prefix sums Hi) and t = pT+r gives a recursion on rows of size ≈ N/p, depth logpN, base case N < p (fully invertible). At e = 1 this collapses back to plain Lucas. The prototype checked it against exact binomials for 400 random (p, e, N, t) cases with p ≤ 101, e ≤ 4.

The chunked accumulating remainder tree

All of this is assembled by sorting every (target, modulus) item by target and cutting the sorted list into chunks whose modulus product has ≈ m bits. Each chunk builds a product tree of its moduli, advances the (P,T,D) state to the chunk's first target by binary splitting in groups of g = ⌊m/log₂N⌋ steps, then descends the tree — at each node, the left child inherits the state mod its own modulus, and the right child's state is advanced from the left target to its own, mod its own (smaller) modulus. A leaf outputs (P,T,D) mod its item's modulus. Nothing outside one chunk's O(m)-bit working set is ever held at once. In a memory-faithful implementation, items are generated on the fly in target order via a segmented sieve, rather than built as one global list up front — this streaming construction is what crates/pi-digits/src/nthdigit2.rs's stream_needs_and_chunks implements.

Complexity proved, modulo the stated model

Model: w = Θ(log n)-bit words, and b-bit multiplication costs M(b) = O(b log b loglog b) bit operations. Summing the accumulating-remainder-tree cost, the Lucas/p-adic tables, and balancing the B and C parts of the series against each other (M = Θ(max(4, N log²m loglog m / m)), i.e. M ∝ n/m up to logs) gives:

\[ T(n, m) = O\!\left( \frac{n^2 \cdot \log n \cdot \log^2 m \cdot \operatorname{loglog} m}{m \cdot \log^2(n/m)} \right) \text{ word operations, memory } O(m \log m) \text{ bits (}O(m)\text{ with care).} \]

For m = nΘ(1), log²m = Θ(log²n), and this is exactly Gourdon's stated bound. At m = √n it gives O(n3/2 log n loglog n), his first special case. The log²(n/m) denominator is the fingerprint: it comes from N² with N ∝ n/log M and M ∝ n/m, i.e. directly from balancing B against a binary-splitting C part — the same balancing act Theorem 1 already does, just with an adjustable knob. conjecture We're fairly confident this is the algorithm Gourdon had in mind; his handling of small primes may have differed, and a loglog from Σ1/p (as in his own Lemma 1) would fit the same bound just as well.

Implementation

The certification path is not behind any abstraction seam: it is plain u128 fixed-point integer code, unchanged across bignum backends. Each term (x mod m)/m is stored as a u128 fixed-point fraction f/2128, computed by exact 128-by-64 long division, so its representation error is < 2−128 (one ulp). Terms accumulate via wrapping add/subtract, which is exact arithmetic modulo 1 on this encoding, so accumulation order never matters. The certification bound (error_units) adds one ulp per term to Gourdon's own truncation bound π/(2eM)N < 10−(n+n₀); n₀ is capped at 24, and long digit requests are split into independent chunks so no single evaluation needs more precision than that. Two cases fall outside this fixed-point loop entirely and go to an exact fallback instead, itself certified the same way (agreement across a verified bracket, guard doubled and retried on disagreement): small positions (below 2,000, where Gourdon's method doesn't apply), and a rare boundary case at any position where a long run of equal digits pushes n₀ past what the u128 accumulator can certify. Both use an exact integer Machin sum (pi_ref); the rare large-n boundary case uses MPFR instead under the native CLI's gmp backend, where it's much faster.

digits(n, count) never returns a digit it can't certify this way: it computes x = frac_10n_pi(n, n₀) with n₀ = count + guard (guard starts at 4), then re-extracts the requested digits from x, x+err and x−err. If all three agree, the digits are certified; if the true value sits too close to a run of 9s or 0s for the guard to resolve, the guard doubles and the whole computation retries.

The memory story at n = 10⁷

Three implementation stages, same position, same machine, peak resident memory:

  • measured 812.1 MiB — the first port: a full O(N) factorisation table and a full sorted item list, held before any chunk ran.
  • 447 MiB (reported, not re-measured here) — after streaming item generation removed the global tables, generating chunks on the fly. This is the task brief's own figure for that intermediate stage; see docs/nthdigit.md for why it wasn't separately re-derived.
  • measured 69.2 MiB — after a memory-profiling pass found the streamed version's real remaining cost wasn't where its own docs blamed it: a p-adic recursion memo table that leaked across an entire prime's worth of queries instead of clearing between them (roughly 40 of 58 MiB at n=10⁶ on its own), and small-prime need-lists that stored every matching (k,t) pair instead of the compact arithmetic-progression range each one actually forms. Both fixed.

What's left, and still genuinely O(N) rather than O(m): each target's cofactor — the single leftover prime factor of mk above √(max mk), when there is one. Unlike a small sieve prime, a cofactor is essentially unique to its own k (or its mirror), not shared by a residue-class arithmetic progression, so it resists the same compaction the small primes got. It is now the dominant named remaining term but is small in absolute terms — tens of MiB, not hundreds, through n = 10⁷. Eliminating it fully would need an external (disk-backed) sort of cofactor needs by target, or a classification that doesn't require computing a cofactor's value before knowing whether it counts as small; neither is implemented.

Benchmarks

measured AMD Ryzen 7 5700G, 6 cores, 2026-09-23. Gourdon's own pidec figures (Pentium III 900 MHz, 2003, Theorem 1 only) are included for historical reference — not an apples-to-apples comparison across ~25 years and two different implementations, but the only published timing baseline for this problem.

Wall-clock time vs digit position (log–log)
10,000100,0001,000,00010,000,00010 ms100 ms1 s10 s100 s1,000 s10,000 s100,000 sDigit position nWall-clock timepidec 2003 (Pentium III, 2003)Theorem 1Theorem 2
Peak memory vs digit position (log–log)
10,000100,0001,000,00010,000,0001 MiB10 MiB100 MiBDigit position nPeak memory (MiB)Theorem 1Theorem 2
Benchmark table: time and memory for Theorem 1, Theorem 2, and Gourdon's pidec
Position n Theorem 1 time Theorem 1 memory Theorem 2 time Theorem 2 memory pidec 2003 (P3, Thm 1 only)
10,00034 ms5.2 MiB16 ms5.7 MiB3.13 s
100,0001.51 s5.1 MiB217 ms7.3 MiB185.1 s
1,000,000113.4 s5.0 MiB4.17 s14.1 MiB15,869 s (4h 24m)
3,000,000——19.3 s22.7 MiB—
10,000,0007,107 s (1h 58m)4.2 MiB111.0 s (1m 51s)69.2 MiB—

Theorem 2's memory grows with position (it's the whole point — it's spending memory to buy speed); Theorem 1's stays flat at a few MiB, genuinely O(log² n). Theorem 2 is faster at every position measured from 10,000 up, by a growing margin: about 2× at 10,000, roughly 64× at 10,000,000.

Verification

Position convention

Two conventions coexist and are easy to conflate: the CLI's pihunt digit <pos> is 1-based (position 1 is the first digit after the decimal point); the library functions (nthdigit::digits, nthdigit2::digits) return positions n+1 ..= n+count, so the CLI calls them with n = pos − 1. Every digit string on this page is a CLI position.

Digits at each CLI position, and at position+1, for cross-checking the convention
pihunt digit positiondigitsposition + 1
10485667227965667227966
10564126002434126002437
10613092756283092756283
3·10636970679156970679155
10772591513362591513361

10⁴–3·10⁶ regenerated with the CLI on 2026-09-23, Theorem 1 and 2 agreeing; 10⁷ both columns MPFR-verified.

MPFR agreement

measured Both theorems' digits are checked against MPFR (via rug) at positions 10⁴, 10⁵ and 10⁶, and independently cross-checked with a second binding, gmpy2's const_pi, at 10⁶ and 10⁷. All agree, and Theorem 1 and Theorem 2 agree with each other at every position tested, including the Feynman point (position 762, a run of six 9s) and hundreds of sequential and random positions up to 20,000 and beyond, across multiple memory-budget settings for Theorem 2. The full pi-digits suite runs a second time with a completely independent pure-Rust bignum backend (no GMP), still checked against MPFR, to catch anything backend-specific.

Browser checks

The race panel above compares the two WebAssembly lanes directly and flags any disagreement; for positions up to 200,000 it also checks the result against a shipped reference file (pi-200k.txt), independently generated and MPFR cross-checked, marking each run ✓ or ✗.

Errors we caught

Three real mistakes surfaced during this work, in the interest of being honest about how this reconstruction was actually built:

  • A missing rounding term in the certification bound. An earlier version of the error budget omitted the one-ulp-per-term fixed-point rounding contribution. It was only unsound for n₀ ≳ 28 — long --count requests or a pathological boundary retry — caught in review and fixed.
  • Doc position labels mixing the CLI and library conventions. One session briefly relabelled the 107 row as 2591513361. That value is correct too — it's pihunt digit 10000001, the library call at n=10,000,000, one position later than every other row — but nothing was wrong in the code, only the label. Both 107 strings are MPFR-verified (see the table above).
  • A verification script that used int() on gmpy2 floating-point values, which rounds to the nearest integer rather than truncating toward zero. A digit-extraction check built on that assumption can silently read off the wrong last digit at a rounding boundary; fixed by truncating explicitly instead of relying on gmpy2's int().

Limits and caveats

  • measured Memory is not the strict O(m) the bound promises in the asymptotic sense: the cofactor term above is still O(N), just small in practice (tens of MiB, not hundreds, through n = 10⁷). It hasn't been tested at, say, n = 10⁹.
  • This is a reconstruction and a verified implementation, not a formal proof that it matches Gourdon's own method, and not a claim that his proof (which we've never seen) looked like this one.
  • conjecture That this is close to Gourdon's own unpublished algorithm is a belief, not a fact — it's unknowable without his notes. What's provable is that this algorithm achieves his stated bound.
  • The p-adic tables are O(p) words in principle, with p reaching √(2(M+1)N); in practice p-adic treatment only ever applies to primes ≤ √(max mk), so the measured peak is far smaller than the theoretical worst case.

Reproduce it

This all lives in the pihunt source tree (github.com/grantstephens/pihunt), in crates/pi-digits/src/nthdigit.rs (Theorem 1) and crates/pi-digits/src/nthdigit2.rs (Theorem 2). Theorem 2 is the CLI's default method.

git clone https://github.com/grantstephens/pihunt
cd pihunt
cargo build --release

# Theorem 2 (default)
pihunt digit 1000000

# Theorem 1, for comparison
pihunt digit 1000000 --method thm1

# full test suite
cargo test --release -j 4

Postscript: the base-10 BBP hunt

Before this, the same tooling searched for a base-10 BBP-type formula for π — a series that would give decimal digits as directly as BBP gives hex ones. Over two thousand candidate shapes were searched with PSLQ across bases 10, 100 and 1000, with degrees up to 3 and extra constants (log 2, log 3, log 5, π², Catalan's constant, ζ(3)); every candidate relation was re-verified at twice the search's working precision. Nothing was found. Degree-1 (Machin-type) formulas for π are already excluded in any base that isn't a power of two (Borwein, Borwein & Galway, 2004), and higher-degree columns are periods of higher weight than π itself — conditionally on a standard, unproved conjecture about weights of periods, any relation would have to reduce to a degree-1 one anyway, which is already ruled out. So the search's null result is what the weight argument predicts, not a surprise waiting to be overturned by more CPU time. Stated plainly, the degree-1 exclusion itself is not fully checked either: it's unverified whether the Borwein–Borwein–Galway theorem covers every degree-1 BBP shape or only the Machin-type subfamily. That's the closed finding that pointed this project at attacking the cost of existing decimal digit extraction instead — which is what Theorem 2 is.

References

  • X. Gourdon, Computation of the n-th decimal digit of π with low memory, 11 Feb 2003. PDF.
  • F. Bellard, Computation of the n'th digit of pi in any base in O(n²).
  • S. Plouffe, arXiv:0912.0303; arXiv:2201.12601.
  • Y. Cheng, G. Hanrot, E. Thomé, E. Zima, P. Zimmermann, Time- and space-efficient evaluation of some hypergeometric constants, arXiv:cs/0701151.
  • V. Yakhontov, arXiv:1209.5097.
  • D. Bailey, P. Borwein, S. Plouffe, On the Rapid Computation of Various Polylogarithmic Constants, Math. Comp. 66 (1997) — the BBP formula.
  • J. Borwein, P. Borwein, W. Galway, Finding and excluding b-ary Machin-type BBP formulae, Canad. J. Math. 56 (2004).