Skip to content
AgileLogics
7 min read

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: Text instead of String, Map/HashMap instead of association lists, Vector or unboxed arrays for numeric work, ByteString for binary data.
  • Make accumulators and record fields strict.
  • Use Int instead of Integer where overflow can't happen, and Double instead of generic Num a in hot loops — or add {-# SPECIALIZE #-} pragmas and INLINE for small hot functions.
  • Check GHC's output with -ddump-simpl only when profiling points at a specific function.