I thought I understood the basics of fold performance in Haskell, as described in foldr, foldl, foldl' on the Haskell Wiki and many other places. In particular, I learned that for accumulating functions, one should use foldl', to avoid space leaks, and that the standard library functions are written to respect this. So I presumed that simple accumulators like length, applied to simple lists like replicate n 1, should require constant space (or at least sub-linear) in the length of the list. My intuition was that on sufficiently simple lists, they would behave roughly like a for loop in an imperative language.
But today I found that this seems not to hold in practice. For instance, length $ replicate n 1 seems to use space linear in n. In ghci:
ghci> :set +s
ghci> length $ replicate (10^6) 1
1000000
(0.02 secs, 56,077,464 bytes)
ghci> length $ replicate (10^7) 1
10000000
(0.08 secs, 560,078,360 bytes)
ghci> length $ replicate (10^8) 1
100000000
(0.61 secs, 5,600,079,312 bytes)
ghci> length $ replicate (10^9) 1
1000000000
(5.88 secs, 56,000,080,192 bytes)
Briefly, my question is: Do length and other strict folds really use linear space? If so, why? And is it inevitable? Below are more details of how I’ve played around trying to understand this, but they’re probably not worth reading — the tl;dr is that the linear-space usage seems to persist whatever variations I try.
(I originally used sum as the example function. As Willem Van Onsem points out, that was a badly-chosen example as default instances aren’t actually strict. However, the main question remains, since as noted below, this occurs with plenty of other functions that really are based on strict folds.)
- Replacing
lengthwithfoldl' (\n _ -> n+1) 0appears to make performance worse by a constant factor; space usage still seems to be linear. - Versions defined with
foldlandfoldrhad worse memory usage (as expected), but only by a small constant factor, not asymptotically worse (as most discussions seem to suggest). - Replacing
lengthwithsum,last, or other simple accumulators, or with the obvious definitions of these usingfoldl', also doesn’t seem to change the linear space usage. - Using
[1..n]as the test list, and other similar variations, also seems to make no significant difference. - Switching between the general versions of
sum,foldl', etc fromData.Foldable, the specialised ones inData.List, and local versions defined directly by pattern-matching, also seems to make no difference. - Compiling instead of working in
ghcialso only seemed to improve space usage by a constant factor. - Switching between several recent versions of GHC — 8.8.4, 8.10.5, and 9.0.1 — also seemed to make no significant difference.