criterion

repository·master·Indexed 19 days ago

https://github.com/haskell/criterion

A Haskell library for accurate microbenchmarking featuring high-resolution analysis, automated noise detection, and statistical reporting in HTML, CSV, and JSON formats. It provides tools for benchmarking pure functions and IO actions using normal form (nf, nfIO) and weak head normal form (whnf, whnfIO) evaluation. The package includes the criterion-measurement core for custom analysis front-ends and supports OLS regression, GC statistics via +RTS -T, and detailed visual reports including Kernel Density Estimate (KDE) charts.

Tokens
3.1K
Snippets
12
Records
16
Agent score
18%

What's inside criterion

  1. Interpret benchmark statistical data

    master

    Benchmark results include several key metrics. For high-quality results, look for an R² goodness-of-fit value between 0.99 and 1.0. Values below 0.9 are considered unreliable.

    MetricDescription
    OLS regressionAn estimate of the time for a single execution using an ordinary least-squares model. Usually more accurate than the mean as it eliminates measurement overhead.
    R² goodness-of-fitMeasures how accurately the linear regression model fits the observed data.
    Mean execution timeThe average execution time calculated from the iterations.
    Standard deviationThe statistical dispersion of the execution times.
    Lower/Upper BoundsCalculated via bootstrapping; they represent the 95% confidence interval for the estimates.
  2. Understand Criterion benchmark charts

    master

    Criterion HTML reports provide two primary visual representations for each benchmark:

    1. Kernel Density Estimate (KDE) Chart: A graph of the probability of any given time measurement occurring. Spikes indicate frequent measurements at that specific time.
    2. Raw Measurements Chart: Shows the number of loop iterations on the x-axis and measured execution time on the y-axis. A linear regression line is drawn through the data points to help identify trends and noise.
  3. Measure garbage collector statistics

    master

    To measure and regress against GHC garbage collector (GC) statistics, you must explicitly enable statistics collection at runtime using the GHC runtime system flag +RTS -T.

    Without this flag, metrics like allocated, numGcs, and other GC-related data will not be available.

    # Example of running a benchmark with GC statistics enabled
    ./my-benchmark +RTS -T
  4. How to write a benchmark suite

    master

    A Criterion benchmark suite is composed of Benchmark values, typically organized into groups. You use defaultMain to run the suite and bgroup to cluster related benchmarks under a common name.

    To define an individual benchmark, use the bench function, which takes a descriptive name and a Benchmarkable value (the code to be measured).

    Example structure:

    main = defaultMain [
      bgroup "fib" [
        bench "1"  $ whnf fib 1,
        bench "5"  $ whnf fib 5
      ]
    ]
    main = defaultMain [
      bgroup "fib" [
        bench "1"  $ whnf fib 1
                   , bench "5"  $ whnf fib 5
                   , bench "9"  $ whnf fib 9
                   , bench "11" $ whnf fib 11
                   ]
      ]
  5. Generate HTML reports from JSON data using criterion-report

    master

    To post-process benchmark data into a visual HTML report, use the criterion-report executable.

    1. Run your benchmark with the --json flag to save the results to a file.
    2. Use criterion-report to convert that JSON file into an HTML report.
    3. You can optionally use the --template flag (supported by Criterion) to customize the report.
    # 1. Run benchmark and save to JSON
    criterion --json data.json
    
    # 2. Generate HTML report
    criterion-report data.json report.html
  6. Get started with a Criterion benchmark

    master

    To create a benchmark suite, import Criterion.Main and use the defaultMain function. defaultMain accepts a list of Benchmark values. You can group related benchmarks using bgroup and define individual benchmarks using bench.

    To ensure you are measuring the actual computation and not just the creation of a thunk, use whnf (Weak Head Normal Form) to evaluate the function to its head normal form during the benchmark.

    {- cabal:
    build-depends: base, criterion
    -}
    
    import Criterion.Main
    
    -- The function we're benchmarking.
    fib :: Int -> Int
    fib m | m < 0     = error "negative!"
          | otherwise = go m
      where
        go 0 = 0
        go 1 = 1
        go n = go (n - 1) + go (n - 2)
    
    -- Our benchmark harness.
    main = defaultMain [
      bgroup "fib" [
        bench "1"  $ whnf fib 1
      , bench "5"  $ whnf fib 5
      , bench "9"  $ whnf fib 9
      , bench "11" $ whnf fib 11
      ]
      ]
  7. Identify and troubleshoot noisy or bogus benchmark results

    master

    If external factors are making measurements noisy, look for these 'yellow flags':

    • Low R² value: An R² goodness-of-fit measure dropping below 0.9 suggests the regression does not fit the data well.
    • Severe outliers: In HTML reports (using --output), look for data points sitting far from the linear regression line.
    • Loose confidence bounds: If the lower and upper bounds on an estimate are not 'tight' (far from the estimate), noise is likely affecting the result.
    • Outlier variance warning: Criterion may explicitly print a warning if the standard deviation is heavily inflated by outliers, e.g., variance introduced by outliers: 91% (severely inflated).
  8. Benchmark IO actions with nfIO and whnfIO

    master

    To benchmark IO actions, Criterion provides two primary functions to control evaluation depth:

    1. nfIO: Evaluates the IO action to normal form (NF). This ensures all internal constructors are fully evaluated and no thunks remain. Use this when dealing with lazy I/O to avoid resource leaks or when you need to ensure full evaluation.

      • Signature: nfIO :: NFData a => IO a -> Benchmarkable
    2. whnfIO: Evaluates the IO action to weak head normal form (WHNF). This only evaluates the outermost constructor. This is efficient for simple values like Int or complex structures like a Map where the outer constructor is sufficient.

      • Signature: whnfIO :: IO a -> Benchmarkable

    Warning: Using whnfIO with lazy I/O functions like readFile can cause resource exhaustion (e.g., Too many open files) because the file handle remains open until the entire content is read.

    import Criterion.Main
    
    main = defaultMain [
        bench "readFile" $ nfIO (readFile "GoodReadFile.hs")
      ]
  9. Benchmark pure functions with nf and whnf

    master

    Because of Haskell's lazy evaluation, you cannot benchmark a fully saturated pure function directly (the work would only happen once). Instead, you must benchmark an unsaturated function by providing all but one argument to the benchmarking function.

    1. nf: Applies the function to the argument and evaluates the result to normal form (NF).

      • Signature: nf :: NFData b => (a -> b) -> a -> Benchmarkable
    2. whnf: Applies the function to the argument and evaluates the result to weak head normal form (WHNF).

      • Signature: whnf :: (a -> b) -> a -> Benchmarkable

    Decision Guide:

    • Use nf if the result is a lazy structure and you want to simulate a real-world consumer that uses the whole structure.
    • Use whnf for simple types like Int or when evaluating the outermost constructor is sufficient.
    main = defaultMain [
      bgroup "fib" [
        bench "1"  $ whnf fib 1,
        bench "5"  $ whnf fib 5
      ]
    ]
  10. Export benchmark data in various formats

    master

    Criterion can export benchmark results into several formats:

    • HTML: Use --output <file> (the most user-friendly format).
    • JSON: Use --json <file> or --template json --output <file>.
    • CSV: Use --csv <file>.
    • JUnit XML: Use --junit <file> (compatible with JUnit-style XML readers).
    $ ./Fibber --json mydata.json
    $ ./Fibber --csv mydata.csv
    $ ./Fibber --junit mydata.xml
  11. Read Criterion command line output

    master

    When running benchmarks in a terminal, the output follows this format:

    benchmarking <name>
    time                 <estimate>   (<lower bound> .. <upper bound>)
                         <R²>         (<lower bound> .. <upper bound>)
    mean                 <estimate>   (<lower bound> .. <upper bound>)
    std dev              <estimate>
    • time is the OLS regression estimate.
    • is the goodness-of-fit metric.
    • mean and std dev are the average execution time and standard deviation respectively.
    benchmarking ByteString/HashMap/random
    time                 4.046 ms   (4.020 ms .. 4.072 ms)
                         1.000 R²   (1.000 R² .. 1.000 R²)
    mean                 4.017 ms   (4.010 ms .. 4.027 ms)
    std dev              27.12 μs   (20.45 μs .. 38.17 μs)