Chapter 28 — Key Takeaways (Profiling and Benchmarking)

A one-page reference for timing code, profiling with gprof, classifying a kernel, measuring cache behaviour, and running a benchmark you can defend — the chapter where you learn to measure before you optimize.

The one rule

Measure first. Profile before you optimize; optimize only the hot loop the profiler names; and never optimize a loop you have not proven is hot. Human intuition about where time goes is reliably wrong.

Timing: the two intrinsics

Intrinsic Measures Call form
cpu_time(t) CPU-seconds consumed (differences only) real(dp) :: t0,t1; call cpu_time(t0); …; call cpu_time(t1)
system_clock(count, count_rate, count_max) wall-clock ticks → seconds see the idiom below

The system_clock idiom — memorize it exactly:

use, intrinsic :: iso_fortran_env, only: dp => real64, int64
integer(int64) :: c0, c1, rate
call system_clock(count_rate=rate)          ! READ the rate — never hard-code it
call system_clock(count=c0)
! ... work ...
call system_clock(count=c1)
wall = real(c1 - c0, dp) / real(rate, dp)   ! seconds
  • Use int64 counters → high-resolution clock, and a count_max so large it will not wrap in a run.
  • Read count_rate from the call; do not assume 1000, $10^6$, or $10^9$.
  • Wrap-around guard (only needed for default-integer clocks): if (ticks < 0) ticks = ticks + count_max + 1.
  • Both intrinsics: only differences are meaningful — the absolute value is an arbitrary origin.

Wall vs CPU time — reading the gap

Observation Meaning
wall $\approx$ CPU (both large) healthy compute-bound run — now profile it
wall $\gg$ CPU I/O-bound or contended — the CPU is waiting, not computing
CPU $>$ wall multithreaded — CPU-seconds summed across cores

Profiling with gprof

$ gfortran -std=f2018 -Wall -O2 -pg -g prog.f90 -o prog   # -pg on compile AND link; keep -O2
$ ./prog                                                   # writes gmon.out
$ gprof ./prog gmon.out > profile.txt

Flat profile columns: % time (self, sorted by this) · cumulative · self seconds · calls (exact) · self ms/call · total ms/call. The % time column is usually the whole answer. Call graph: callers above the indexed line, callees below; self vs children splits the time. gfortran mangles names: laplacian_, MAIN__.

Classify the hot loop

Hot loop — the small region (often one innermost loop) where most of the runtime lives. Memory-bound — limited by data movement; the FP units idle, waiting. Compute-bound — limited by arithmetic; data arrives faster than it is crunched. Arithmetic intensity $I = \text{flops}/\text{byte}$; below ~1 ⇒ memory-bound.

Kernel $I$ (flop/byte) Class Fix (chapter)
axpy y=y+a*x ~0.08 memory-bound bandwidth-limited; little to do
5-point stencil ~0.4 memory-bound loop order, blocking (Ch. 29)
naïve matmul low (poor reuse) memory-bound blocking (Ch. 29)
tuned dgemm high (blocked reuse) compute-bound already optimal (Ch. 21)

Diagnose which: (1) count flops/bytes; (2) add a cheap register-only flop and re-time — no slowdown ⇒ memory-bound; (3) measure the cache-miss rate (cachegrind).

Cache behaviour with cachegrind

$ valgrind --tool=cachegrind ./prog          # simulated cache; deterministic; ~20-100x slower
$ cg_annotate cachegrind.out.<pid>           # per-line Dr / D1mr breakdown
  • Reports D refs, D1 misses, LLd misses, and miss rates. A high D1/LL miss rate ⇒ memory-bound.
  • Same D refs, different miss rate ⇒ same work, different loop order (locality).
  • Cachegrind = why & where (reproducible, generic cache). perf stat -e cache-misses = how much (real HW).

Benchmarking methodology

Checklist: warm up (≥1 untimed run) · repeat (≥ ~20) · take the minimum (best case) or median (typical) · report the spread · build -O2/-O3 · quiet machine, one variable at a time · consume the result (or the compiler deletes the loop) · record compiler, flags, machine.

  • min = least-disturbed run (noise only slows). median = robust to one outlier (unlike the mean).
  • Dead-code trap: an unused result lets -O2 delete the timed loop → fake zero time. Print/accumulate it.
  • Reset mutated state between repetitions, or you time different work each run.

The 80/20 rule and the size of the prize

  • Most runtime lives in a small fraction of code (often 90/10 or 99/1 in numerical codes).
  • Speeding a fraction $p$ by factor $K$ ⇒ whole-program speedup $S = \dfrac{1}{(1-p) + p/K}$.
  • Ceiling ($K\to\infty$): $S_{\max} = 1/(1-p)$. A routine at 20% caps the program at $1.25\times$.
  • Knuth: "premature optimization is the root of all evil" — optimize the critical few percent, after profiling; leave the rest clear. (Amdahl's Law formalizes the ceiling — Ch. 31.)

Pitfalls

  • Profiling the -O0 build — measures the wrong balance; -O2 inlines helpers away. Profile what you ship.
  • Guessing count_rate or using default-integer counters — wrong or negative times.
  • Timing an un-warmed run — measures cache/page/frequency start-up, not steady state.
  • Reporting one sample — an anecdote, not a measurement.
  • Optimizing before profiling — you will spend the critical 3% of effort on the wrong 3% of code.

Flags introduced

Flag / command Purpose
-pg compile + link for gprof instrumentation
-g keep symbols (readable names, source annotation)
gprof exe gmon.out produce flat profile + call graph
valgrind --tool=cachegrind / cg_annotate simulate cache; annotate misses per line
perf stat -e cache-misses real hardware performance counters

The heat-solver piece added

timers.f90 / timerstic() / toc() result(seconds) wrapping the system_clock idiom; module state is saved by design. Used to time the step loop and establish the serial baseline per-step time — the figure of merit Chapters 29, 30, and 38 drive down. gprof confirms the hot loop is the stencil in laplacian.

Numbers worth memorizing

  • Elapsed = (count_end − count_start) / count_rate. Read the rate; use int64.
  • $I < 1$ flop/byte ⇒ memory-bound (the stencil, and most array kernels).
  • Speedup ceiling $= 1/(1-p)$ for a hot fraction $p$ — compute it before optimizing.