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.
(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.
(a) Each sample "represents" Δt=10ms=0.010s of wall time. Total run time =NΔt=5000×0.010s=50s.
(b) Fraction owned by the function =nf/N=1500/5000=0.30. So
Tself=NnfTtotal=0.30×50s=15s,
which is also just nfΔt=1500×0.010=15s. Both routes agree, as they must.
Recall Solution
Per-call cost =1063.60s=3.6×10−6s=3.6μs.
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 106 attacks the whole 3.60 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:
IPC=cyclesinstructions=3.0×10109.0×109=0.30.CPI = Cycles Per Instruction, the exact inverse — how many clock ticks each instruction costs on average:
CPI=IPC1=0.301≈3.33.
IPC of 0.30 is far below the ~4 ceiling (equivalently CPI 3.33 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.
(a) Let s→∞: the term p/s→0, so
Speedupmax=1−p1=0.281≈3.57×.
No matter what, the untouched 28% caps you at 3.57×.
(b) With s=4:
Speedup=(1−0.72)+40.721=0.28+0.181=0.461≈2.17×.
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
cache miss rate=1.5×1091.8×108=0.12=12%.branch miss rate=2.0×1092.0×107=0.01=1%.
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 ≈1.8×108×120=2.16×1010 cycles.
Fraction of all cycles =3.0×10102.16×1010=0.72=72%.
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.
(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)p=nf/N=2000/4000=0.50. (Sanity: NΔt=4000×0.010=40 s ✓ matches the stated run.)
(b)Speedup=(1−0.5)+30.51=0.5+0.161=0.661=1.5×.(c) New time =1.540s≈26.7s.
This is the forecast-then-verify discipline: predict 26.7 s before editing, then re-profile to confirm — if the measured time isn't near 26.7 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 (pA=0.50, sA=1.6):
SpeedupA=(1−0.50)+1.60.501=0.50+0.31251=0.81251≈1.231×.B (pB=0.30, sB=5):
SpeedupB=(1−0.30)+50.301=0.70+0.061=0.761≈1.316×.Pick B (1.316×>1.231×). The lesson: a smaller slice with a bigger achievable speedup can beat a larger slice you can barely improve — Amdahl rewards p/s shrinking, so both pands matter, not p alone.
At -O2 the optimizer inlinedtiny_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)q^=ng/N=40/400=0.10 (10%).
(b)σq^=Nq^(1−q^)=4000.10×0.90=4000.09=2.25×10−4=0.015.
So q^±2σq^=0.10±0.03, i.e. the true fraction plausibly lies in [0.07,0.13]. (Check the normal-approximation conditions first: Nq^=40≥5 and N(1−q^)=360≥5, 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 — N=400 is enough to conclude "over budget". Had the interval straddled 5% you would run longer: σq^ falls like 1/N, so to halve the error you need 4× the samples. This is why short profiling runs give jittery hot-lists.
Recall Solution
(a) With instruction count fixed, run time of q∝ cycles =instructions/IPC, so speedup of q=IPColdIPCnew=0.352.8=8×.
(b)Speedup=(1−0.60)+80.601=0.40+0.0751=0.4751≈2.11×.(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 2.11×, 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 ::: Speedup=1/((1−p)+p/s)
Self time from samples ::: Tself=(nf/N)Ttotal=nfΔt
Standard deviation of a sampled fraction ::: σq^=q^(1−q^)/N
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, =1/IPC
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