Posts
Adding up ten million numbers
Recursive summation in single precision loses four digits over ten million terms. Pairwise and compensated summation keep them for little extra work.
A floating-point sum looks like the most innocent computation in a program. It is computed one addition at a time, , and every addition rounds its result to the nearest representable number. Each rounding is tiny, but a long sum performs millions of them, and their errors do not have to cancel.
How large can the error get?
#For recursive summation, the classic bound is1
where is the unit roundoff: in double precision and in single precision. The bound is pessimistic because it assumes every rounding error points the same way. It still grows with , and for positive terms the partial sum grows too, so later terms are added to an ever larger total and lose more of their low-order bits.
Single precision is where this stops being academic. Sensor data, graphics, and machine-learning code store float32 arrays as a matter of course, and terms is an ordinary array.
An experiment
#I drew values uniformly from as float32 and summed them three ways, comparing each result with math.fsum, which returns the correctly rounded sum of the same values:
- recursive: one addition after another, as a plain loop does;
- pairwise:
np.sum, which splits the array into blocks and adds partial sums in a tree; - compensated: Kahan’s algorithm, below.
|
Relative errors of the single-precision results:
| Terms | Recursive | Pairwise (np.sum) | Compensated |
|---|---|---|---|
At ten million terms the recursive sum is about 4,999,406 and off by 127, which is 255 units in the last place. The compensated sum was correctly rounded at every size: no single-precision number lies closer to the exact sum. Pairwise summation was correctly rounded at every size but one, where it was a single unit in the last place away.
In double precision the same experiment is quieter: the recursive sum of terms is off by a relative , and both other methods match math.fsum exactly. The problem has not gone away; it has moved to longer sums and to data with cancellation.
Why compensation works
#Kahan’s algorithm keeps a second variable for the part of each addition that rounding throws away.2 In exact arithmetic, line 9 computes . In floating-point arithmetic, is the part of that actually made it into the sum, so subtracting leaves the negative of what was lost. Line 7 adds it back to the next term before that term is added to the total.
The bound for compensated summation no longer grows with at first order:
Pairwise summation reaches a similar effect differently. Adding numbers in a balanced tree means that each term passes through about additions instead of up to , so the factor in the first bound becomes .
What it costs
#Compensated summation performs four floating-point operations per term instead of one, and the loop carries a dependency through compensation that limits how much a processor can overlap. In interpreted Python the difference disappears in the interpreter’s overhead; in compiled code it is real but often hidden by memory traffic, because a long sum is limited by how fast the data arrive.
The practical advice is short. Prefer library reductions, which usually sum pairwise. Write a compensated loop when you must accumulate in a loop, in single precision, or over many terms. Never assume that a sum is accurate because each addition is.