Intuition The one question this page answers
A profiler like perf or gprof never watches your whole program. It peeks at it a few thousand times and then makes a confident claim like "compute_dist is 72% of your runtime." How can a handful of glances know that? This page builds that claim from absolute zero — one picture at a time — until the formula T self ( f ) = N n f T total feels obvious.
We are deriving the central result of the parent note :
T self ( f ) ≈ n f Δ t = N n f T total
Every symbol will be earned before it is used .
Intuition A program is a timeline
WHAT. Forget code for a second. A running program is just a line of time , left to right. At every instant the CPU is inside exactly one function . So the timeline is coloured in bands: this stretch is inside compute_dist, this stretch is inside normalize, and so on.
WHY start here. We cannot talk about "how much time a function takes" until we picture time as a length we can measure. Time-as-length is the whole trick.
PICTURE. Below, the horizontal black arrow is the total run. Each labelled band is one function. The red band is the function we care about, compute_dist — notice it takes up a big chunk.
Definition Two lengths we will keep referring to
T total = the length of the whole timeline (wall-clock seconds of the run).
T self ( f ) = the combined length of all bands belonging to function f — the time spent inside f 's own body , not its callees.
The true answer we want is T self ( compute_dist ) : literally, add up the lengths of every red band. A perfect tool would do exactly that. The problem: measuring every band edge exactly is expensive (that is instrumentation , and it distorts timing). So we cheat cleverly.
Intuition Stop-motion the timeline
WHAT. Instead of tracking every band edge, we set an alarm that rings at fixed intervals Δ t (say every 10 ms ). Each time it rings, we look at the timeline and write down only one thing : which function is active right now? Everything between rings is ignored.
WHY this tool. A fixed-interval alarm turns a hard measurement (exact band lengths) into a simple counting problem (how many rings landed on red?). Counting is cheap; the alarm interrupt costs almost nothing. This is why perf overhead is ~1–5% while full simulation (Callgrind ) is 20–100×.
PICTURE. The red vertical lines are the alarm "clicks", evenly spaced by Δ t . Below each click, a dot marks which band it fell into.
Definition Symbols born here
Δ t = the fixed gap between two consecutive alarm clicks (the sampling period ).
A sample = one alarm click plus the note "function active here".
N = total number of clicks over the whole run.
n f = number of clicks that landed inside function f .
Read the picture: if there were N = 10 clicks and n red = 4 of them landed on the red band, then compute_dist "caught" 4 out of 10 clicks.
Intuition One click "owns" the gap after it
WHAT. Because clicks are Δ t apart, we let each click stand for the Δ t of time around it . A click inside a red band is our evidence that "roughly Δ t of red time happened here."
WHY. This is the leap from counting clicks to estimating time . A count is a pure number; multiplying by Δ t converts it back into seconds — the thing we actually care about. Units guide us: clicks × click seconds = seconds .
PICTURE. Each red click now grows a shaded box of width Δ t beneath it. Stack the red boxes and you get an estimate of the red total.
Intuition Why fractions are nicer than raw seconds
WHAT. The whole run has N clicks, and every click owns Δ t , so the whole run is T total ≈ N Δ t . Divide the two equations and Δ t cancels .
WHY do this. Δ t is a tool-internal knob (100 Hz? 1000 Hz?). We would rather report something knob-independent. The fraction of samples is exactly that — it is what a profiler prints as % time.
PICTURE. Two stacked bars: the top is all N clicks (the full timeline), the bottom is just the n f red clicks. The red bar's share of the top bar is the percentage.
Worked example Plug in the parent's numbers
The parent's gprof row says compute_dist = 72% self time in a run where its self time is 3.60 s . If the whole run were T total = 5.00 s , then
N n f = 0.72 ⟹ T self = 0.72 × 5.00 = 3.60 s . ✓
The fraction and the seconds agree — exactly as the derivation promises.
Intuition Fewer clicks = shakier answer
WHAT. Each click is a coin-flip-like event: "did it land on red or not?" With few clicks the fraction n f / N jitters; with many clicks it settles down toward the true share.
WHY this matters. This is the honest fine print behind every profiler number . A function that shows "3% self time" over 200 samples might really be anywhere from 1% to 6% — noise, not signal. The cure is a longer run or higher sampling rate (more N ).
PICTURE. Three dartboards of increasing N : the estimated red fraction (red dot) drifts toward the true dashed line as N grows.
n f = 0 (function never caught)
N 0 × T total = 0 . The formula says zero self time . Trap: this does not prove f took zero time — a fast function between two clicks can be stepped over entirely . Below, the red band is real but no click lands on it. Fix: raise the sampling rate or use Callgrind (exact counts, no missing).
n f = N (one hot loop dominates)
N N × T total = T total : every click is red, so f is ~100% self time. This is the ideal profiling result — a single clear culprit. By Amdahl's law the whole run's speedup ceiling is then almost unbounded, so optimizing f is fully worth it.
Worked example Case C — many bands, one function, tiny
Δ t
As Δ t → 0 (clicks infinitely dense), N → ∞ , samples touch every band, and n f Δ t → the exact summed red length. The approximation becomes exact in the limit. This is precisely what Callgrind does — it is the Δ t → 0 end of our picture, paid for with 20–100× slowdown.
Common mistake Reading self time as total time
Why it feels right: main grabs many clicks because control passes through it. Trap: most of main's clicks are really inside its callees' bands, not main's own body. Our derivation counts a click for the innermost active function — that is why main's self time is tiny even when its total is huge. Optimize by self time ; use total time only to find pricey subtrees.
Everything above collapses into one image: a timeline, evenly-spaced red clicks, red clicks counted into n f , divided by N , multiplied by T total , out comes the self time. Trace the arrows.
Recall Feynman retelling — the whole walkthrough in plain words
Picture your program's run as a long ruler laid on the floor, painted in coloured stripes — each stripe is time spent inside one function, and the stripe we care about is red. We don't want to measure every stripe edge (too fussy, and touching it disturbs the timing). So instead we set a metronome that ticks every Δ t seconds. On each tick we glance down and note which colour we're standing on. Count the red ticks (n f ) and all the ticks (N ). Since ticks are evenly spaced, each tick "owns" a Δ t slice of floor, so red time ≈ (red ticks) × Δ t . Dividing by the whole thing lets Δ t vanish, leaving a clean fraction of ticks — that's the "% time" your profiler prints. It's only an estimate because a tick can miss a thin stripe entirely (that's why a function can show 0%); take more ticks and the wobble shrinks like 1/ N ; take infinitely many and you get Callgrind's exact answer.
Recall Quick self-check
Why does Δ t disappear from the final formula? ::: Both self time (n f Δ t ) and total time (N Δ t ) carry one factor of Δ t , so it cancels in the ratio n f / N .
A function shows 0 self samples — is its true time zero? ::: No. A fast function can fall entirely between two clicks and be missed; raise the rate or use exact-count Callgrind.
To halve the statistical error of a percentage, how many more samples? ::: About 4× more, because error scales like 1/ N .
The Δ t → 0 limit of this picture corresponds to which tool? ::: Callgrind — exact, deterministic counts, at 20–100× slowdown.
Related: Compiler Optimization Flags (-O2, -O3, inlining) · Cache Hierarchy & Locality of Reference · Branch Prediction & Pipelining · Debugging with gdb · Build Systems & Toolchain