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
| Source | What 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:
| Part | Condition | Method |
|---|---|---|
| main | product of all pe with p > t | the accumulating-remainder-tree item above (target t, modulus q) — Dt is invertible here by construction |
| Lucas | p ≤ t, p ∥ mk | Lucas' theorem, reducing to one small residue mod p (below) |
| p-adic | p ≤ 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.mdfor 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.
| Position n | Theorem 1 time | Theorem 1 memory | Theorem 2 time | Theorem 2 memory | pidec 2003 (P3, Thm 1 only) |
|---|---|---|---|---|---|
| 10,000 | 34 ms | 5.2 MiB | 16 ms | 5.7 MiB | 3.13 s |
| 100,000 | 1.51 s | 5.1 MiB | 217 ms | 7.3 MiB | 185.1 s |
| 1,000,000 | 113.4 s | 5.0 MiB | 4.17 s | 14.1 MiB | 15,869 s (4h 24m) |
| 3,000,000 | — | — | 19.3 s | 22.7 MiB | — |
| 10,000,000 | 7,107 s (1h 58m) | 4.2 MiB | 111.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.
pihunt digit position | digits | position + 1 |
|---|---|---|
| 104 | 8566722796 | 5667227966 |
| 105 | 6412600243 | 4126002437 |
| 106 | 1309275628 | 3092756283 |
| 3·106 | 3697067915 | 6970679155 |
| 107 | 7259151336 | 2591513361 |
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
--countrequests 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'spihunt 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'sint().
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).