Exercises — Performance profiling — CPU, memory, I - O profiling
Level 1 — Recognition
Goal: name the concept. No arithmetic.
L1.1 A profiler slows your program by 5× because it hooks every function call and return, counting each exactly. What kind of CPU profiler is this — deterministic or sampling?
L1.2 time ./prog prints real 8.0s, user 0.4s, sys 0.3s. Total CPU is s, but wall-clock (real) is s. Is this program CPU-bound or I/O-bound?
L1.3 In a flame graph, what does the width of a bar represent, and therefore which bar is the hotspot?
Recall Solution — L1
L1.1 Deterministic (tracing) profiler. The giveaways: (a) it hooks every call/return, and (b) it slows the program by a large factor (2–10×). A sampling profiler would instead peek at intervals and add almost no overhead. Note two properties of a sampling profiler we will lean on in L5: it has low overhead (peeks at intervals, does not hook every call), and it typically attaches to an already-running process without instrumenting or recompiling the code.
L1.2 I/O-bound. Using the decomposition from the definition above, . Here s but total CPU is only s, so s of pure waiting (disk/network/lock/sleep). A CPU profiler would show your code "idle" and mislead you.
L1.3 Width = time (cumulative time of that call frame). The widest bar is the hotspot — the frame the program spends the most time inside. See the flame-graph figure in L3.1 below and Flame Graphs.
Level 2 — Application
Goal: plug numbers into Amdahl's Law and the sampling error formula.
L2.1 A request takes ms. Of this, ms is database access, which you make faster. What is the overall speedup , and the new request time?
L2.2 A function is of runtime. You optimize it infinitely (). What is the maximum possible overall speedup?
L2.3 You want to resolve a hotspot whose true fraction is to a standard error of (one percentage point). How many samples do you need?
Recall Solution — L2
L2.1 First find : the fraction is , and . New time ms. (Sanity check the long way: unchanged part ms, DB part ms, total ms. ✓)
L2.2 As , the term , so Even making that part instant only gets you . The other of runtime is untouched, and it becomes the new floor.
L2.3 Rearrange . Square both sides: , so At a 1 kHz sampling rate that is only s of profiling.
Level 3 — Analysis
Goal: decide why the profiler shows what it shows, and what to do.
L3.1 A flame graph shows handle_request (100% width) calling serialize (15%), db_query (78%), and log (7%). Inside db_query the bar is flat, wide, with no children, and CPU utilization during it is near zero. Which function should you optimize, and is the fix a code fix or an I/O fix?
L3.2 Your deterministic profiler reports that tiny_helper() has the highest self-time. But tiny_helper is a two-line function called million times. A sampling profiler ranks it far lower. Which profiler do you trust and why?
L3.3 A parent function build_report shows 95% cumulative time but only 3% self time. Should you optimize build_report itself? What does this pattern tell you?
Recall Solution — L3
L3.1 Optimize ==db_query== (78% width — it dominates). The flame graph below makes it visual: bar width is time, and db_query is the widest bar. The pattern flat + wide + no children + low CPU is the signature of waiting (I/O-bound). So the fix is an I/O fix (add a DB index, batch an N+1 query, or cache the result) — not a Python micro-optimization. Optimizing serialize (15%) would be chasing the wrong bar; by Amdahl its ceiling is .

L3.2 Trust the sampling profiler here. A deterministic profiler hooks every call/return, so a function called million times pays that per-call overhead million times — its measured time is inflated by the measurement itself. This distorts the ranking specifically for hot, call-heavy functions. The sampling profiler adds no per-call cost and reflects reality. See Benchmarking vs Profiling.
L3.3 No — do not optimize build_report directly. Recall from the self-vs-cumulative definition above: cumulative time = self time + all descendants. A cumulative / self split means build_report is a thin wrapper (a wide bar with only a sliver of self time): the real work () is burning in a callee it invokes. Descend into the children and sort by self time to find the true hotspot.
Level 4 — Synthesis
Goal: combine Amdahl, self/cumulative time, and the wall-vs-CPU split in one decision.
L4.1 A request is ms wall-clock. Profiling reveals: ms CPU parsing, ms CPU formatting, and ms waiting on 8 sequential database queries (an N+1 pattern) at ms each. You have two options:
(a) Rewrite parsing+formatting in C to make that CPU work faster. (b) Batch the 8 queries into 1 query taking ms.
Compute the overall speedup and new time for each option. Which wins, and why does the CPU rewrite disappoint?
L4.2 After you apply option (b), what is the new dominant cost, and what would the next Amdahl calculation look like if you then made the remaining CPU work faster?
Recall Solution — L4
Setup. Total ms (CPU: 50 parse + 50 format) (I/O wait). The figure below shows all three time-splits side by side.

Option (a) — speed CPU work . The CPU part is , with : New time ms. Verify directly: CPU ms, I/O stays ms, total ms. ✓ A huge code win bought only a request win — because the CPU was only of the time (Amdahl's ceiling ).
Option (b) — batch 8 queries into 1. The I/O part is ; batching queries of ms into query of ms cuts ms, i.e. speed factor : New time ms. Verify directly: CPU ms + I/O ms ms. ✓
Winner: option (b), vs . The CPU rewrite disappoints because you optimized the small slice (); Amdahl caps you no matter how brilliant the rewrite. Always attack the biggest first — which here is the I/O, exactly what the wall ≫ CPU gap told you to expect.
L4.2 After (b), new total ms: CPU ms + I/O ms. CPU is now the dominant cost, . Making that CPU work faster: New time ms. The moral: the same CPU rewrite that was worthless before is now worth — because fixing the big slice changed which slice is biggest. Re-profile after every fix.
Level 5 — Mastery
Goal: derive, design, and reason about limits.
L5.1 (Derive the sample budget for a ratio of two hotspots). Two functions have true fractions and . You want enough samples that you can confidently distinguish which is bigger — say, the gap should be at least standard errors wide. Using the worst-case error (take , which maximizes , so each fraction has ), find the that makes , where the difference of two independent estimates has .
L5.2 (Design decision). You must profile a production service handling live traffic. The service is latency-sensitive: a slowdown would trip its timeouts and cause an outage. Which profiler type do you deploy, and state the two properties of that profiler (from L1) that make it safe here. Then name the one thing it cannot give you that a deterministic profiler could.
Recall Solution — L5
L5.1 First, why is the worst case? The quantity is a downward parabola in : it is at and , and symmetric about the midpoint. Its derivative is , which is zero at — so the maximum of sits at , where . Since grows with , using gives the largest (safest) error estimate, guaranteeing enough samples no matter the true .
Why the errors add in quadrature (the independence assumption). is the variance-addition rule and it holds only if the two estimates are independent. Here that is an approximation, not an exact truth: at any single sample the profiler records one running function, so the counts for func1 and func2 are actually negatively correlated (a sample credited to one cannot be credited to the other). That negative covariance would make the true smaller than the quadrature value. So treating them as independent is the conservative (pessimistic) choice — it slightly over-estimates the error and hence over-budgets , which is exactly what we want for a safe "how many samples is enough" bound. We proceed with the independent approximation knowing it errs on the side of caution.
Each estimate, using the worst-case , has . For the difference of two (assumed-independent) estimates the errors add in quadrature: Now require and solve for step by step: Divide both sides by : Square both sides (both sides positive, so the inequality direction is preserved): Take reciprocals (flips the inequality) and multiply by : So samples (worst-case). At 1 kHz that is s of profiling to reliably tell a hotspot from a one. Notice the cost of resolving a small gap between two large hotspots is much higher than resolving one big hotspot alone (L2.3 needed only for one function) — distinguishing near-ties is genuinely expensive. (And since the independence assumption over-estimated the error, is a safe upper bound; the truly-needed count is a touch lower.)
L5.2 Deploy a sampling (statistical) profiler (e.g. py-spy, Linux perf). The two safety properties, both stated in the L1 solution:
- Low overhead — it only peeks at intervals (e.g. every 1 ms) instead of hooking every call, so it does not cause the slowdown that would trip timeouts. A deterministic profiler, by contrast, hooks every call/return and can slow the service 2–10×, which here means an outage — so it is disqualified.
- Attaches to an already-running process without instrumenting/recompiling — you don't restart the live service or add hooks to its code; the profiler observes from outside a process that is already serving real traffic.
The one thing it cannot give: an exact call count (and exact per-call timing). Sampling is a statistical estimate — it can miss short-but-frequent functions that fall between snapshots and never reports "this ran exactly times." For exact call counts or precise per-call timing you must run a deterministic profiler in a safe offline/test environment, never on the latency-sensitive production box. In production, pair the sampling profiler with tracing/observability for the request-level picture.
Recall One-line takeaways
Amdahl's Law ranks a fix by ::: the fraction it touches, not by its local speedup. Wall ≫ CPU means the program is ::: I/O-bound (waiting), so a CPU profiler will mislead. High cumulative but low self time means the function is ::: a wrapper — the real work is in a callee. Sampling error shrinks as ::: , so big hotspots resolve fast but near-ties are costly. The safe production profiler is ::: a low-overhead sampling profiler that attaches to a live process.