5.3.16 · D4Build Systems & Toolchain

Exercises — Profiling — gprof, perf, Valgrind - Callgrind

3,920 words18 min readBack to topic

The recurring maths tools you will use here are worth naming once, in plain words — every symbol defined, every unit stated — before any problem uses them.

Figure — Profiling — gprof, perf, Valgrind - Callgrind
Figure — Profiling — gprof, perf, Valgrind - Callgrind

Level 1 — Recognition

Recall Solution

(a) gprof — the -pg flag makes the compiler weave in the mcount() counter, which is instrumentation (exact call counts, but it changes timing). (b) Callgrind (a Valgrind tool) — the whole program executes on a simulated CPU, so counts are deterministic but it runs 20–100× slower. (c) perf — event-based sampling driven by real PMU counters, ~1–5% overhead.

Recall Solution

Only gprof strictly requires a recompile (you must add -pg). perf works on an existing binary (adding -g and -fno-omit-frame-pointer only improves symbol/call-graph quality). Callgrind also needs no recompile — it instruments the binary at run time via the Valgrind VM. Adding -g just gives nicer source-line attribution.

Recall Solution

main's total time = its own body plus everything it calls. A near-zero self time means main itself does almost nothing — its huge total is just the sum of its children. Do not optimize main. You optimize by self time (where the CPU actually sits); total time is only for spotting an expensive subtree to drill into.


Level 2 — Application

Recall Solution

(a) Each sample "represents" of wall time. Total run time . (b) Fraction owned by the function . So which is also just . Both routes agree, as they must.

Recall Solution

Per-call cost . With a call count this large, the first question is not "make each call faster" but "can I make fewer calls?" — memoize repeated inputs, hoist the call out of a loop, or vectorize. Reducing calls from attacks the whole s, whereas shaving each call only chips at the constant.

Recall Solution

IPC = Instructions Per Cycle, how many instructions the core finishes each clock tick: CPI = Cycles Per Instruction, the exact inverse — how many clock ticks each instruction costs on average: IPC of is far below the ~4 ceiling (equivalently CPI means each instruction drags on for over 3 cycles), so the core spends most cycles stalling (idle, waiting). This points at memory or branch problems — see L3 — not at needing a cleverer algorithm.


Level 3 — Analysis

Recall Solution

(a) Let : the term , so No matter what, the untouched caps you at . (b) With : So a 4× local win buys a 2.17× global win — well short of the 3.57× ceiling, telling you further effort on this one function has sharply diminishing returns.

Recall Solution

The branch predictor is doing fine (1%). A 12% cache miss rate is the culprit: a DRAM miss costs ~100+ cycles vs ~4 for an L1 hit, so those misses easily explain the low IPC. The fix lives in Cache Hierarchy & Locality of Reference — improve locality (struct-of-arrays, loop blocking) — not algorithmic complexity. Contrast with Branch Prediction & Pipelining, which is not the problem here.

Recall Solution

Cycles lost to misses cycles. Fraction of all cycles . With ~72% of cycles plausibly consumed by memory stalls, cache behaviour alone accounts for the vast majority of the slowdown — memory is indeed a sufficient explanation, and locality work is the right lever. The figure below turns this arithmetic into a picture of where the cycle budget goes.

Figure — Profiling — gprof, perf, Valgrind - Callgrind

Level 4 — Synthesis

Recall Solution

(a) Callgrind — it is deterministic (same input → identical instruction counts), so a >2% threshold is stable across machines with no timer noise. Slowness (20–100×) is acceptable in CI. (b) gprof — quick and classic; a -pg rebuild plus one command gives a flat profile good enough for "roughly which function". (c) perf — real PMU hardware counters at ~1–5% overhead, no recompile of the release binary, and it reports true cache misses on the real CPU. Callgrind is disqualified because its simulated timing is not real-hardware timing.

Recall Solution

(a) . (Sanity: s ✓ matches the stated run.) (b) (c) New time . This is the forecast-then-verify discipline: predict s before editing, then re-profile to confirm — if the measured time isn't near s, your model (or your assumption that only serialize changed) is wrong. See Benchmarking & Microbenchmark Pitfalls.

Recall Solution

Apply the Amdahl formula to each candidate separately. A (, ): B (, ): Pick B (). The lesson: a smaller slice with a bigger achievable speedup can beat a larger slice you can barely improve — Amdahl rewards shrinking, so both and matter, not alone.


Level 5 — Mastery

Recall Solution

At -O2 the optimizer inlined tiny_helper into its callers (see Compiler Optimization Flags (-O2, -O3, inlining)). Once inlined, it is no longer a distinct function with an entry point, so gprof's mcount()/sample attribution has nothing to pin time to — its cost is now fused into the caller's self time. Your -O0 profile pointed at code that does not exist in the shipped binary. Correct method: profile the same optimization level you ship (-O2/-O3), add -g for source symbols, and -fno-omit-frame-pointer so perf can still build call graphs. Never draw conclusions from a -O0 profile.

Recall Solution

No contradiction — they measure different things.

  • Callgrind counts instructions executed: f runs few instructions (8%).
  • But perf shows f triggers 90% of cache misses; each miss stalls the core ~100+ cycles doing no instructions, crushing IPC to 0.4.
  • gprof measures wall-clock self time, which includes those stall cycles, so f eats 40% of real time despite few instructions. Coherent story: f is memory-bound — cheap in instructions, expensive in time because it waits on DRAM. This is exactly why the mistake "Callgrind tells me real seconds" is dangerous: low instruction count hid a real-time hog. Use perf for the timing truth here.
Recall Solution

(a) (10%). (b) So , i.e. the true fraction plausibly lies in . (Check the normal-approximation conditions first: and , so the bell-curve interval is trustworthy here.) (c) The whole interval sits above 5%, so even at the pessimistic end g exceeds the budget — is enough to conclude "over budget". Had the interval straddled 5% you would run longer: falls like , so to halve the error you need the samples. This is why short profiling runs give jittery hot-lists.

Recall Solution

(a) With instruction count fixed, run time of q cycles , so speedup of q . (b) (c) After the rewrite, re-run perf and confirm q's measured self time actually dropped ~8× (equivalently its cycles/miss-rate fell as modelled). If the whole-program speedup isn't near , either IPC didn't reach 2.8 or another function became the new bottleneck — profile again. This closes the measure → model → change → re-measure loop.


Recall Quick self-quiz (cloze)

Amdahl's speedup formula ::: Self time from samples ::: Standard deviation of a sampled fraction ::: IPC of 0.3 on a 4-wide core means the CPU is mostly ::: stalling (waiting), likely on cache misses What does CPI stand for and equal? ::: Cycles Per Instruction, The tool with deterministic, machine-independent counts ::: Callgrind The tool with ~1–5% overhead on real hardware ::: perf You must optimize by which time column, not total? ::: self time