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·02.11.26
← Back to news

A decades-old bug in Knuth's long division (TAOCP Vol II, Algorithm 4.3.1D)

Original article
kolja.rs|130 points|24 comments|by nk_kolja|Aug 13, 2026

A Decades-Old Bug in Knuth's Long Division (TAOCP Vol II, Algorithm 4.3.1D)

By Novak Kaluđerović

I recently discovered a flaw in a cornerstone of computer science: Algorithm D from Donald Knuth's The Art of Computer Programming. Not only did this lead to a correction in the TAOCP errata, but it also resulted in a theorem bearing my own name.

📌 Overview of the Journey

  • The discovery of the bug
  • Fundamentals of long division
  • Hardware constraints of multiprecision integers
  • The hierarchy of division reduction
  • The specific failure in Theorem B
  • The LLVM "bug" and AI's failure
  • Further optimizations (Stronger bounds, constant division)

🚀 How I Got Here

While preparing for a technical interview, I embarked on a side project: creating a library for arithmetic over prime fields. This required implementing fixed-size multiprecision integers with a focus on:

  • Constant-time execution
  • Constant memory access patterns
  • Suitability for modern cryptographic protocols

During development, I adopted a strict personal mantra:

"Avoid the div instruction at all costs."

In the world of cryptography, runtime division is almost never used. Instead, developers substitute division with a combination of multiplication and bit-shifting. While multiplication is relatively straightforward—almost an axiom of natural numbers—division is far more complex.

The Complexity of Division

Division is not universally defined (e.g., division by zero). What we actually perform is division with remainder, which yields two distinct values:

  1. The quotient (qqq)
  2. The remainder (rrr): the smallest non-negative difference between the dividend and a multiple of the divisor.

The definition of "smallest" is critical; changing the size function used here would fundamentally alter the division algorithm.

Performance Gap

The practical reason for avoiding division is performance. On modern CPUs:

  • mul (Multiply): ≈1–2\approx 1\text{--}2≈1–2 cycles, fully pipelined.
  • div (Divide): ≈20\approx 20≈20 cycles, typically non-pipelined.

Eventually, I reached the point where I couldn't avoid it: I needed a full multiprecision division algorithm. Naturally, I turned to the gold standard: Donald Knuth's TAOCP.


🧮 Long Division from Scratch

Multiprecision Integers in Hardware

Since cryptographic integers often span hundreds or thousands of bits, they exceed the capacity of a single CPU register. We store them in base bbb, where each "limb" is one machine word:

x=(xn−1,…,x0)b=∑i=0n−1xibi,0≤xi<bx = (x_{n-1}, \dots, x_0)_b = \sum_{i=0}^{n-1} x_i b^i, \quad 0 \le x_i < bx=(xn−1​,…,x0​)b​=∑i=0n−1​xi​bi,0≤xi​<b

The foundation of these algorithms rests on four primitive operations:

OperationInputsOutputsLogic
addcx,yx, yx,ys,carrys, \text{carry}s,carrys=(x+y)(modb)s = (x+y) \pmod bs=(x+y)(modb); carry=1\text{carry}=1carry=1 if overflow
subcx,yx, yx,yd,borrowd, \text{borrow}d,borrowd=(x−y)(modb)d = (x-y) \pmod bd=(x−y)(modb); borrow=1\text{borrow}=1borrow=1 if underflow
mulx,yx, yx,yhi,lohi, lohi,lox⋅y=hi⋅b+lox \cdot y = hi \cdot b + lox⋅y=hi⋅b+lo
div(hi,lo),y(hi, lo), y(hi,lo),y(q1,q0),r(q_1, q_0), r(q1​,q0​),rhi⋅b+lo=q⋅y+r,0≤r<yhi \cdot b + lo = q \cdot y + r, \quad 0 \le r < yhi⋅b+lo=q⋅y+r,0≤r<y

Notably, the div primitive is the outlier. While multiplying two limbs always results in a two-limb product, dividing a two-limb dividend by a one-limb divisor requires a two-limb quotient (q=q1⋅b+q0q = q_1 \cdot b + q_0q=q1​⋅b+q0​) and a remainder.

The Reduction Strategy

The goal is to divide a multiprecision integer uuu by vvv to find qqq and rrr such that: u=q⋅v+rand0≤r<vu = q \cdot v + r \quad \text{and} \quad 0 \le r < vu=q⋅v+rand0≤r<v

To simplify this, we use a hierarchy of reductions:

The Setup:

  1. Assume the divisor vvv has nnn limbs (top limb vn−1≠0v_{n-1} \neq 0vn−1​=0).
  2. Pad the dividend uuu with leading zeros so it has n+m+1n+m+1n+m+1 limbs.
  3. Requirement: The leading nnn limbs of uuu must be strictly less than vvv: ⌊u/bm+1⌋<v\lfloor u / b^{m+1} \rfloor < v⌊u/bm+1⌋<v If this isn't true, we simply add another leading zero to uuu.

This ensures the quotient qqq consists of exactly m+1m+1m+1 limbs.


🐛 The Bug

While implementing Algorithm D, I encountered Theorem B. The proof felt... off. It was overly convoluted for a simple statement and isolated a special case that didn't seem like a true corner case.

I attempted to prove the theorem myself. I succeeded I failed.

That failure provided the key: a counterexample to Algorithm D. This bug had remained undetected for decades. Consequently, I have now contributed a corrected theorem to the TAOCP errata.

Visual Evidence

Below are the corrections and the discrepancies found in the original text:

The New Theorem B: Theorem B (N. Kaluđerović, 2026), added to the TAOCP errata

The Corrected Step D3: step_d3_errata

The "Meticulous" Original vs. The Error: meticulous step_D3_old


🔍 Analysis and Aftermath

How did it stay hidden?

The bug is subtle. It requires a very specific set of inputs to trigger a failure in the quotient estimation. Most test suites likely didn't hit the exact edge case where the estimate q^\hat{q}q^​ is off by more than one in the specific direction that breaks the algorithm.

The LLVM "Bug"

While documenting this, I discovered that the implementation of this logic in LLVM also contained a similar quirk. While not necessarily a "bug" in the sense of crashing, it mirrored the logic flaw found in Knuth's text.

AI's Failure

I tested several modern LLMs to see if they could spot the error in Theorem B. None of them could. They all confidently asserted that the original proof was correct, proving that deep mathematical verification still requires human intuition.

Moving Forward

The discovery has led me to explore more robust ways to handle division, including:

  • Stronger Bounds: Improving the initial guess for q^\hat{q}q^​.
  • Quotient Limb Doubling: Optimizing the number of limbs processed per iteration.
  • Constant Divisors: Using reciprocal multiplication to eliminate division entirely.