[Haskell-cafe] Reasoning about performance
pakin at lanl.gov
Wed Sep 4 00:28:49 CEST 2013
I'm a Haskell beginner, and I'm baffled trying to reason about code
performance, at least with GHC. For a program I'm writing I needed to
find all pairs of elements of a list. That is, given the list "ABCD"
I wanted to wind up with the list
the order of elements in the resulting list is immaterial, but I don't
want both ('A', 'B') and ('B', 'A'), for example.
My first attempt looked like this:
allPairs1 :: [a] -> [(a, a)]
allPairs1  = 
allPairs1 (x:xs) = map (\a -> (x, a)) xs ++ allPairs1 xs
Benchmarking with ghci's ":set +s" reveals the following performance
(median of three runs shown):
GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> :!ghc -c -O2 allpairs.hs
Prelude> :load allpairs
Ok, modules loaded: AllPairs.
Prelude AllPairs> :m +Control.DeepSeq
Prelude Control.DeepSeq AllPairs> :show modules
AllPairs ( allpairs.hs, allpairs.o )
Prelude Control.DeepSeq AllPairs> :set +s
Prelude Control.DeepSeq AllPairs> deepseq (allPairs1 [1..10000]) True
(4.85 secs, 4004173984 bytes)
A colleague suggested getting rid of the list append as follows:
allPairs2 :: [a] -> [(a, a)]
allPairs2  = 
allPairs2 (x:xs) = allPairs2' x xs xs
where allPairs2' x   = 
allPairs2' x (y:ys) zs = (x,y) : allPairs2' x ys zs
allPairs2' x  (z:zs) = allPairs2' z zs zs
Not surprisingly, that runs faster:
Prelude Control.DeepSeq AllPairs> deepseq (allPairs2 [1..10000]) True
(2.14 secs, 4403686376 bytes)
I then figured I could do even better by rewriting the code
allPairs3 :: [a] -> [(a, a)]
allPairs3  = 
allPairs3 (x:xs) = allPairs3' x xs xs 
where allPairs3' :: a -> [a] -> [a] -> [(a, a)] -> [(a, a)]
allPairs3' h (x:xs) ys result = allPairs3' h xs ys ((h, x):result)
allPairs3' _  (y:ys) result = allPairs3' y ys ys result
allPairs3' _   result = result
This takes half the memory as the above (yay!) but runs substantially
*slower* (and appears to be thrashing my computer's memory system):
Prelude Control.DeepSeq AllPairs> deepseq (allPairs3 [1..10000]) True
(10.58 secs, 2403820152 bytes)
What gives? Why does my tail-recursive implementation run even slower
and presumably produce more garbage than my initial, naive
implementation? Is there some optimization GHC is performing for
allPairs1 and allPairs2 that it can't apply to allPairs3?
Similarly, how should I, as a newcomer who wants to write efficient
Haskell code, reason about whether a code change is likely to improve
rather than degrade performance? Guidelines like "per-iteration
appends = bad" and "tail recursion = good" are great, but the
preceding data indicate that something subtler is taking precedence
over those guidelines with respect to code performance.
Thanks in advance,
More information about the Haskell-Cafe