Skip to content
DataLeaf

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, s^k=fl⁡(s^k−1+xk)\hat s_k = \operatorname{fl}(\hat s_{k-1} + x_k), 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

∣sn−s^n∣≤(n−1) u∑i=1n∣xi∣+O(u2), \lvert s_n - \hat s_n \rvert \le (n - 1)\, u \sum_{i=1}^{n} \lvert x_i \rvert + O(u^2),

where uu is the unit roundoff: 2−53≈1.1×10−162^{-53} \approx 1.1 \times 10^{-16} in double precision and 2−24≈6.0×10−82^{-24} \approx 6.0 \times 10^{-8} in single precision. The bound is pessimistic because it assumes every rounding error points the same way. It still grows with nn, 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 10710^7 terms is an ordinary array.

An experiment

#

I drew nn values uniformly from [0,1)[0, 1) 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.
python

Relative errors of the single-precision results:

Terms nnRecursivePairwise (np.sum)Compensated
10310^{3}5.1×10−75.1 \times 10^{-7}2.9×10−82.9 \times 10^{-8}2.9×10−82.9 \times 10^{-8}
10410^{4}1.7×10−71.7 \times 10^{-7}2.8×10−82.8 \times 10^{-8}2.8×10−82.8 \times 10^{-8}
10510^{5}6.6×10−66.6 \times 10^{-6}2.4×10−82.4 \times 10^{-8}2.4×10−82.4 \times 10^{-8}
10610^{6}8.6×10−68.6 \times 10^{-6}4.3×10−84.3 \times 10^{-8}2.0×10−82.0 \times 10^{-8}
10710^{7}2.5×10−52.5 \times 10^{-5}1.9×10−81.9 \times 10^{-8}1.9×10−81.9 \times 10^{-8}

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.

Relative error of three summation methods against the number of terms, on logarithmic axes.
Figure 1. Relative error of the single-precision sum against the number of terms. Pairwise and compensated summation stay below the unit roundoff.

In double precision the same experiment is quieter: the recursive sum of 10710^7 terms is off by a relative 1.8×10−141.8 \times 10^{-14}, 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 (t−total)−y=0(t - \text{total}) - y = 0. In floating-point arithmetic, t−totalt - \text{total} is the part of yy that actually made it into the sum, so subtracting yy 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 nn at first order:

∣sn−s^n∣≤(2u+O(nu2))∑i=1n∣xi∣. \lvert s_n - \hat s_n \rvert \le \bigl(2u + O(n u^2)\bigr) \sum_{i=1}^{n} \lvert x_i \rvert.

Pairwise summation reaches a similar effect differently. Adding numbers in a balanced tree means that each term passes through about log⁡2n\log_2 n additions instead of up to n−1n - 1, so the factor (n−1)(n - 1) in the first bound becomes ⌈log⁡2n⌉\lceil \log_2 n \rceil.

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.


  1. Nicholas J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd edition (SIAM, 2002), Chapter 4, derives these bounds and compares summation methods. ↩︎

  2. William Kahan, “Further remarks on reducing truncation errors”, Communications of the ACM 8, no. 1 (1965): 40. ↩︎