Profiling and Optimization
Prerequisites: Strictness and space leaks
GHC produces fast native code, but you only find the slow parts by
measuring. Always compile with optimization before judging speed:
runghc and ghci interpret code and are many times slower.
Runtime statistics
Any program compiled with -rtsopts can print memory and time
statistics:
-- illustrative: compile with ghc -O2 -rtsopts Sum.hs
import Data.List (foldl')
main :: IO ()
main = print (foldl' (+) 0 [1 .. 10000000 :: Int])
$ ./Sum +RTS -s
50000005000000
47,480 bytes allocated in the heap
1,792 bytes copied during GC
42,856 bytes maximum residency (1 sample(s))
...
Total time 0.000s ( 0.008s elapsed)
(Shortened; exact figures vary by machine.) With -O2, GHC fuses
the list and the fold into a tight loop, so the ten-million-element list
is never actually built — almost nothing is allocated. Maximum
residency — the most live memory at once — is the number to watch for
space leaks.
Cost-centre profiling
ghc -O2 -prof -fprof-auto -rtsopts Main.hs
./Main +RTS -p # writes Main.prof: time and allocation per function
./Main +RTS -hT # heap profile by closure type, no -prof needed
In a cabal project:
cabal build --enable-profiling
cabal run my-app -- +RTS -p
Main.prof lists the cost centres (functions) where time and
allocation went, as a tree. Heap profiles (-h options) show how memory
use changes over time; view them with eventlog2html or hp2ps. For
an interactive view, the ghc-debug and Threadscope tools inspect
live heaps and parallel runs.
Benchmarking
Use the criterion or tasty-bench packages for reliable micro-benchmarks: they run code many times, account for noise, and report the mean with confidence intervals.
-- illustrative: bench/Main.hs with tasty-bench
import Test.Tasty.Bench
fibSlow :: Int -> Integer
fibSlow n = if n < 2 then toInteger n else fibSlow (n - 1) + fibSlow (n - 2)
main :: IO ()
main = defaultMain
[ bench "fib 20" $ nf fibSlow 20
, bench "fib 25" $ nf fibSlow 25
]
nf evaluates the result fully (normal form) on every run, so laziness
doesn't make the benchmark measure nothing.
Optimization checklist
- Pick the right data structure first:
Textinstead ofString,Map/HashMapinstead of association lists,Vectoror unboxed arrays for numeric work,ByteStringfor binary data. - Make accumulators and record fields strict.
- Use
Intinstead ofIntegerwhere overflow can't happen, andDoubleinstead of genericNum ain hot loops — or add{-# SPECIALIZE #-}pragmas andINLINEfor small hot functions. - Check GHC's output with
-ddump-simplonly when profiling points at a specific function.