A decades-old bug in Knuth's long division (TAOCP Vol II, Algorithm 4.3.1D)
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
divinstruction 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:
- The quotient ()
- The remainder (): 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): cycles, fully pipelined.div(Divide): 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 , where each "limb" is one machine word:
The foundation of these algorithms rests on four primitive operations:
| Operation | Inputs | Outputs | Logic |
|---|---|---|---|
addc | ; if overflow | ||
subc | ; if underflow | ||
mul | |||
div |
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 () and a remainder.
The Reduction Strategy
The goal is to divide a multiprecision integer by to find and such that:
To simplify this, we use a hierarchy of reductions:
The Setup:
- Assume the divisor has limbs (top limb ).
- Pad the dividend with leading zeros so it has limbs.
- Requirement: The leading limbs of must be strictly less than : If this isn't true, we simply add another leading zero to .
This ensures the quotient consists of exactly 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:

The Corrected Step D3:

The "Meticulous" Original vs. The Error:

🔍 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 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 .
- Quotient Limb Doubling: Optimizing the number of limbs processed per iteration.
- Constant Divisors: Using reciprocal multiplication to eliminate division entirely.