Skip to content

Commit 5760100

Browse files
Add an introductory doc
1 parent bb3c2df commit 5760100

2 files changed

Lines changed: 127 additions & 0 deletions

File tree

docs/intro.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Haskell Performance Analysis
2+
3+
## GHC RTS Stats
4+
5+
RTS Stats give entire OS process level (not OS thread level) cpu time
6+
and not Haskell thread cpu time. When multiple OS threads are used, the
7+
cpu time recorded is the cpu time of all the threads combined. Also, the
8+
way kernel accounts this time it could be off by a little (microseconds)
9+
because each thread's cpu time is recorded at the last accounting
10+
event. Allocations are recorded by the GHC RTS only at the GC boundary,
11+
so the allocations reported are from the point when the last GC
12+
happened. So we need to be careful when using or interpreting these
13+
stats.
14+
15+
If we built the program without -threaded and we are using a single
16+
Haskell thread then we can get cpu time between any two points in the
17+
program accurately. Accurate accounting of allocations will require a GC
18+
to be forced which is not usually practical.
19+
20+
In a multithreaded program using RTS stats we can only tell time how
21+
much total CPU time (and allocations) the entire Haskell process (all
22+
threads) spent between two points, but we cannot tell which Haskell
23+
thread spent how much time.
24+
25+
## GHC Event logging
26+
27+
Eventlog based Haskell thread aware time and allocation analysis is
28+
possible with stock GHC but there are some limitations and drawbacks
29+
which are fixed in the RTS patch described below. The patch basically
30+
adds accurate information and more information, and we then use a custom
31+
event log analysis program to provide an accurate and comprehensive
32+
picture of the entire program.
33+
34+
TBD: document the exact limitations and differences.
35+
36+
## threadCPUTime# prim op
37+
38+
Available in the
39+
[GHC 9.2.8 RTS patch](https://github.com/composewell/ghc/releases/tag/ghc-9.2.8-perf-counters-1-rc1).
40+
41+
Install the patched GHC using:
42+
43+
```
44+
ghcup install -u https://github.com/composewell/ghc/releases/download/ghc-9.2.8-perf-counters-1-rc1/ghc-9.2.8.20231130-x86_64-unknown-linux.tar.xz ghc
45+
```
46+
47+
This is a very simple and easy to use mechanism. The RTS is modified
48+
such that we record the accurate time and allocation information in a
49+
Haskell thread control block at the points when the thread is scheduled
50+
and descheduled. Thus for each Haskell thread we can always get how much
51+
time the thread spent on CPU and how much allocations it did.
52+
53+
An RTS API is provided to fetch the current thread's accumulated cpu
54+
time and allocation stats. We can collect these stats between point A
55+
and B in a program, diff will tell us the time spent and allocations
56+
between the two points.
57+
58+
We have to ensure that we are diffing the data for the same thread id at
59+
both the points. See [this example program](./threadCPUTime.hs).
60+
61+
The API has some measurement overhead but it is not very high. If we
62+
are nesting measurements be aware that outer measurement will measure
63+
the measurement overhead of the inner one. If you are measuring a
64+
relatively small amount of time then reduce the overhead (approx 2
65+
microseconds and 300 byte allocations, measure the exact value using an
66+
empty code block).
67+
68+
This is very useful in micro-measurements and analysis of the CPU cost
69+
different segments of code in a particular Haskell thread without worrying
70+
about the preemption points of the thread.
71+
72+
By measuring the wall clock time as well at the two points we can find
73+
the idle time for the thread. However, the idle time includes the queue
74+
time and the IO time - it may not be very useful unless we know the
75+
breakup. For that we need to add the facility to measure either the
76+
queue time or the IO time.
77+
78+
Limitations: this allows only thread specific measurements, we cannot
79+
tell what other threads and everything else in the system is doing
80+
between the two points of measurements. It can be added to the patch
81+
though. For accurate synchronization (if needed) of all threads at the
82+
given points we can stop-the-world, can be useful in testing but not a
83+
good idea in production though. Also, managing windows with possible
84+
nesting can complicate the RTS code.
85+
86+
## Eventlog based perf counters
87+
88+
Available in GHC 8.10.7 RTS patch. Can be ported to later GHCs.
89+
90+
This gives you a more comprehensive picture of the entire program
91+
between any two specified points, it gives a detailed report about all
92+
the threads in the system not just the current thread.
93+
94+
See the [README](../README.md) for more details on this.

docs/threadCPUTime.hs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{-# LANGUAGE UnboxedTuples #-}
2+
{-# LANGUAGE MagicHash #-}
3+
4+
import Control.Concurrent (threadDelay)
5+
import GHC.Exts (threadCPUTime#, ThreadId#)
6+
import GHC.Int(Int64(..), Int32(..))
7+
import GHC.IO(IO(..))
8+
9+
-- returns (sec, nsec, words, slices)
10+
getThreadStats :: IO (Int64, Int64, Int32, Int32)
11+
getThreadStats = IO $ \s ->
12+
case threadCPUTime# s of
13+
(# s', sec, nsec, allocs, slices #) ->
14+
(# s', (I64# sec, I64# nsec, I32# allocs, I32# slices) #)
15+
16+
-- returns (nsec, bytes, slices)
17+
stat :: IO (Int64, Int32, Int32)
18+
stat = do
19+
(sec, nsec, words, slices) <- getThreadStats
20+
let tenPow9 = 1000000000
21+
threadCPUTime = sec * tenPow9 + nsec
22+
threadAllocs = words * 8
23+
threadSlices = slices
24+
pure (threadCPUTime, threadAllocs, threadSlices)
25+
26+
main :: IO ()
27+
main = do
28+
(nsec1, bytes1, slices1) <- stat
29+
-- threadDelay 1
30+
(nsec2, bytes2, slices2) <- stat
31+
putStrLn $ "cpu time nsec: " ++ show (nsec2 - nsec1)
32+
putStrLn $ "allocated bytes: " ++ show (bytes2 - bytes1)
33+
putStrLn $ "sched-out count: " ++ show (slices2 - slices1)

0 commit comments

Comments
 (0)