on fe28607 help sign in
streamly/docs/Developer/Benchmarks.md
334 lines 11 kB history raw
1# Benchmarking
3## Build and run benchmarks directly
5```
6$ cabal run bench:Prelude.Serial # run selected
7$ cabal run bench:Prelude.Serial -- --help # help on arguments
8$ cabal run bench:Prelude.Serial -- --stdev 100000 # specify arguments
9$ cabal run bench:Prelude.Serial --flag fusion-plugin # with fusion-plugin
11$ cabal build bench:Prelude.Serial # build selected
12$ cabal build --enable-benchmarks streamly-benchmarks # build all
13$ cabal build --enable-benchmarks all # build all, alternate method
15$ cabal build --flag "-opt" ... # disable optimization, faster build
16```
18## Building and Running Benchmarks with bench-runner
20The executable `bench-runner` is the top level driver for
21running benchmarks. It runs the requested benchmarks and then creates a
22report from the results using the `bench-show` package.
24IMPORTANT NOTE: The first time you run this executable it may take a long
25time because it has to build the `bench-report` executable which has a
26lot of dependencies.
28You can install it once in the root of the repository and use it multiple times.
30You can use `cabal.project.report` to install bench-runner like so:
31```
32$ cabal install bench-runner --project-file=cabal.project.report --installdir=./ --overwrite-policy=always
33$ ./bench-runner <bench-runner-args>
34```
36If you're using nix, you can install bench-runner like so:
37```
38$ cd benchmark/bench-runner
39$ nix-shell --run 'cabal install bench-runner --installdir=../../ --overwrite-policy=always'
40$ cd ../../
41$ ./bench-runner <bench-runner-args>
42```
44You can run the `bench-runner` without installing, like so:
45```
46$ cabal run bench-runner --project-file=cabal.project.report -- <bench-runner-args>
47```
49## bench-runner: Quick start
51Assuming `bench-runner` is the executable. You can replace `./bench-runner` with
52`cabal run bench-runner --project-file=cabal.project.report --`
54Useful commands:
56```
57$ ./bench-runner --help
58$ ./bench-runner --quick # run all the benchmark suites
59$ ./bench-runner --targets help # Show available benchmark suites
60$ ./bench-runner --targets serial_grp # Run all serial benchmark suites
61$ ./bench-runner --targets "Prelude.Serial Data.Parser" # run selected suites
62$ ./bench-runner --no-measure # don't run benchmarks just show previous results
64# Run all O(1) space complexity benchmarks in `Prelude.Serial` suite
65$ ./bench-runner --targets Prelude.Serial --prefix Prelude.Serial/o-1-space
67# Run a specific benchmark in `Prelude.Serial` suite
68$ ./bench-runner --targets Prelude.Serial --prefix Prelude.Serial/o-1-space.generation.unfoldr
69```
71Note: `bench-runner` enables fusion-plugin by default.
73## Comparing results with baseline
75```
76# Checkout baseline commit
77$ ./bench-runner --quick
79# Checkout commit with new changes
80$ ./bench-runner --quick --append
82# To add another result to comparisons just repeat the above command on
83# desired commit
85# To display the current results without running the benchmarks.
86# See "Reporting without measuring" for more info.
87$ ./bench-runner --no-measure
88```
90## Comparing benchmark suites
92First see the available benchmark suites:
94```
95$ ./bench-runner --targets help
96```
98You will see some benchmark suites end with `_cmp`, these are comparison
99groups. If you run a comparison group benchmark, comparison of all the
100benchmark suites in that group will be shown in the end. For example to compare
101all array benchmark suites:
103```
104$ ./bench-runner --targets array_cmp
105```
107## Reporting without measuring
109You can use the `--no-measure` option to report the already measured results in
110the benchmarks results file. A results file may collect an arbitrary number of
111results by running with `--append` multiple times. Each benchmark has its own
112results file, for example the `Prelude.Serial` benchmark has the results file at
113`charts/Prelude.Serial/results.csv`.
115You can also manually edit the file to remove a set of results if you like or
116to append results from previously saved results or from some other results
117file. After editing you can run `bench-runner` with the `--no-measure` option to
118see the reports corresponding to the results.
120## Additional benchmark configuration
122### Stream size
124You can specify the stream size (default is 100000) to be used for
125benchmarking:
127```
128$ cabal run bench:Prelude.Serial -- --stream-size 1000000
129```
131### External input file
133In the `FileSystem.Handle` benchmark you can specify the input file as an
134environment variable:
136```
137$ export Benchmark_FileSystem_Handle_InputFile=./gutenberg-500.txt
138$ cabal run FileSystem.Handle -- FileSystem.Handle/o-1-space/reduce/read/S.splitOnSeq
139```
141The automatic tests do not test unicode input, this option is useful to specify
142a unicode text file manually.
144## Running benchmarks on valid UTF8 input
146To run the unicode benchmarks on valid utf8 input, you can do the following,
148```
149$ Benchmark_FileSystem_Handle_InputFile=<valid-unicode-filepath> ./bench-runner --benchmarks Unicode.Stream --cabal-build-options "-f include-strict-utf8"
150```
152## Benchmarking notes
154We run each benchmark in an isolated process to minimize interference
155of benchmarks and to be able to control the RTS memory restrictions per
156benchmark.
158### Gotchas
160The benchmark driver forces a GC before and after the measurement.
161However, we have observed
162that sometimes the GC stats may not be accurate when the number of iterations
163in the measurement is small (e.g. 1 iteration). In such cases usually the
164number of GCs and GC times would also be 0.
166## Diagnosing Performance Issues
168### Reproducible comparison
170When comparing different compilers we need to make sure that we are
171using exactly the same versions of the libraries for apples to apples
172comparison. We have seen cases where a change in the "random" library
173caused allocations regressions in the new version of compiler because of
174the way in which the benchmark code was generated due to the change.
176When it is required to reproduce benchmark results precisely across
177different systems, it is recommended that you create and use a cabal
178freeze file so that the versions of all libraries are pinned.
180### Identifying issues
182There are two ways to find problematic code:
1841. Run performance benchmarks using `bench-runner`, select the benchmarks
185 that are taking more than expected time.
1862. When making a new change, compare with the baseline and select benchmarks
187 with the most regression reported by `bench-runner`.
189Number of allocations are the most stable measure that do not vary from
190run to run. `cpuTime` and `bytesCopied` may vary. When comparing two
191runs for regression the first thing to look at is the difference in
192allocations. Also note that allocations may vary from run to run for
193concurrent benchmarks.
195The next thing to look at is cpuTime. Please note that cpuTime may
196fluctuate quite a bit, you may want to run the relevant benchmarks
197without the --quick mode for confirming and make sure no other load is
198running on the system when measuring.
200Usually the increase is cpuTime is proportional to the increase in
201allocations but sometimes it may increase independently because more cpu
202instructions are being executed. TBD - we should count the instructions
203instead.
205### Inspection Testing
207Before you proceed make sure have to run the benchmarks with
208`inspection` flag on. It may catch any obvious issues or regressions.
210```
211$ cabal build --flag inspection --flag fusion-plugin --enable-benchmarks streamly-benchmarks
212```
214### Compiling with diagnostics
2161. Comment out all other benchmarks in the given benchmark suite, and
217 keep only the one you are examining.
2182. Edit the file and add the following line on top:
220```
221{-# OPTIONS_GHC
222 -ddump-simpl
223 -ddump-to-file
224 -dsuppress-all
225 -Wmissed-specialisations
226 -Wall-missed-specialisations
227 -fplugin-opt=Fusion.Plugin:verbose=2
228 -fplugin-opt=Fusion.Plugin:dump-core
229#-}
230```
2313. Build the benchmark suite with fusion-plugin enabled:
233```
234$ cabal build bench:Prelude.Serial --flag fusion-plugin
235```
237See the `.dump-simpl` file in the cabal build directory. You can find it
238like this:
240```
241$ find dist-newstyle/ -name "*.dump-simpl"
242```
244Make sure you are looking into the right build dir (`--build-dir` may change
245`dist-newstyle` to something else), and check in the appropriate GHC
246version dir.
248### Compiling standalone example
250Sometimes you may want to create a separate program from the benchmark code
251removing the benchmarking harness to simplify and isolate the code for better
252reasoning and simpler core.
254Add the following GHC options at the top of your file, say, example.hs:
256```
257{-# OPTIONS_GHC
258 -ddump-simpl
259 -ddump-to-file
260 -dsuppress-all
261 -Wmissed-specialisations
262 -Wall-missed-specialisations
263 -fplugin Fusion.Plugin
264 -fplugin-opt=Fusion.Plugin:verbose=2
265 -fplugin-opt=Fusion.Plugin:dump-core
266#-}
267```
269Do not include the optimization options in OPTIONS_GHC pragma, instead, specify
270them on the command line. This is to avoid optimization failing if you import
271another module which is not compiled with the same optimization options.
273```
274$ cabal build # build and write ghc environment file
275$ ghc -O2 -fspec-constr-recursive=16 -fmax-worker-args=16 example.hs
276```
278To pinpoint where the optimization is going wrong you can examine the
279plugin generated core files for each optimization pass. The files are
280numbered for each optimization pass. You can compare successive files
281using side-by-side diff and see what the compiler is doing between each
282pass.
284### Diagnosing the Problem
286#### Specialization Issues
288Look for missed specialization messages. When you are comparing against a
289baseline, check if something that was specialized before is no longer
290specialized.
292In the core you have to look for type class dictionaries e.g.
294```
295exc_r6DD = \ @s_a6ai -> try $fMonadCatchIO $fExceptionSomeException
296```
298Search for `$f` in the core.
300#### Fusion Issues
302Look for unfused function warnings emitted by fusion-plugin. You may
303want to take a look at the unfused constructors or functions that
304fusion-plugin is warning about. Beware that:
306* fusion-plugin emits warnings for unfused stuff in intermediate functions as
307 well, those should be ignored.
308* the constructors may remain genuinely unfused unless the loop is
309 closed. So you should look at the warnings in the file where the loop is
310 closed and everything is supposed to be fused.
312Also, look at the core for unfused constructors. At times you may need
313to look for the boxed primitive type constructors e.g. W8# or I#, these
314may not be eliminated, usually, due to strictness issues.
316Often it is useful to diff and compare the core without the problem and
317the core with the problem especially in cases when the problem is due to
318GHC version changes, or smaller changes in the code.
320Note, some operations are inherently fusion breaking, those cannot fuse,
321they are usually annotated so in their documentation.
323### Resolving the problem
325Review the problematic code, see [the optimization
326guide](/docs/Developer/optimization-guidelines.md) for common problems and how to
327solve those. If no obvious issues are found on review, then generate and
328examine the core.
330You may want to add the `Fuse` annotation on some of those constructors
331to make the code fuse. Please note that unnecessary `Fuse` annotations
332may cause unnecessary inlining. Also, make sure that the constructor you
333are adding fuse annotation is not shared by any other code where you may
334not want inlining/fusion.