AnyLearn
All lessons
Programmingintermediate

Measure First: The Arithmetic That Decides What to Optimise

Most optimisation effort is spent on code that was never the problem, and the reason is that intuition about where time goes is reliably wrong. This lesson covers why guessing fails, the arithmetic that caps what any optimisation can buy, the difference between latency and throughput, and how to set a target that tells you when to stop.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 7

Why intuition fails at this specifically

Developers are good at reasoning about correctness and bad at reasoning about time, and the gap is structural rather than a matter of skill.

The reasons compound:

  • Cost is invisible in the source. One line calling a function looks the same whether that function returns immediately or makes a network call. The code shows structure, not duration.
  • Orders of magnitude are unintuitive. A cache hit and a disk read differ by a factor in the thousands, but they occupy one line each, and the eye weights them equally.
  • Attention follows complexity. The gnarly algorithm gets suspicion; the innocent-looking loop that calls a getter which lazily reloads a config file does not.
  • The hot path is rarely where the interesting code is. Time concentrates in serialisation, allocation, logging and waiting, none of which anyone enjoys reading.

Key idea: the profiler is not a tool for confirming your hypothesis. It is a tool for discovering that your hypothesis was wrong, which it usually is, and the teams that get fast are the ones that check before they change anything.

The cost of guessing is not just wasted effort, and the arithmetic of why is the next step.

Full lesson text

All 7 steps on one page, for reading, reference, and search.

Show

1. Why intuition fails at this specifically

Developers are good at reasoning about correctness and bad at reasoning about time, and the gap is structural rather than a matter of skill.

The reasons compound:

  • Cost is invisible in the source. One line calling a function looks the same whether that function returns immediately or makes a network call. The code shows structure, not duration.
  • Orders of magnitude are unintuitive. A cache hit and a disk read differ by a factor in the thousands, but they occupy one line each, and the eye weights them equally.
  • Attention follows complexity. The gnarly algorithm gets suspicion; the innocent-looking loop that calls a getter which lazily reloads a config file does not.
  • The hot path is rarely where the interesting code is. Time concentrates in serialisation, allocation, logging and waiting, none of which anyone enjoys reading.

Key idea: the profiler is not a tool for confirming your hypothesis. It is a tool for discovering that your hypothesis was wrong, which it usually is, and the teams that get fast are the ones that check before they change anything.

The cost of guessing is not just wasted effort, and the arithmetic of why is the next step.

2. The ceiling on any optimisation

There is a hard limit on what improving one part of a system can do to the whole, and it was formalised by Gene Amdahl in 1967 at the AFIPS Spring Joint Computer Conference.

If a component accounts for a fraction p of total runtime and you speed that component up by a factor s, the overall speedup is:

S=1(1p)+psS = \frac{1}{(1 - p) + \frac{p}{s}}

The consequence that matters arrives when you let s go to infinity. Even making the component take zero time leaves the rest untouched, so the best possible speedup is 1 divided by (1 - p).

Maximum speedup from making one component infinitely fast
x speedup02468101.111.33241010% of runtime25%50%75%90%
Source: computed: 1/(1-p) from Amdahl's law, the limit as the component's speedup goes to infinity

Key idea: optimising a component that is 10 percent of runtime cannot make the system more than about 1.11 times faster, no matter how brilliant the optimisation. This single fact should govern where effort goes, and it is why the first question is never "how do I make this faster" but "what fraction of the total is this?"

3. The arithmetic applied to a real choice

The law becomes useful when it settles an argument that would otherwise be about taste.

Predict first

A request takes 800 ms: 600 ms waiting on a database, 150 ms in JSON serialisation, 50 ms in your business logic. A developer proposes rewriting the business logic in a faster language, expecting a 10x improvement on that part. What happens to the request?

In practice: run this calculation before starting, every time. It takes two minutes, needs only a rough profile, and it routinely reverses the plan. The proposal that survives the arithmetic is usually not the one anybody arrived with.

4. Latency and throughput are different goals

"Make it faster" hides two goals that often pull in opposite directions, and choosing between them changes which optimisations are even correct.

LatencyThroughput
MeasuresTime for one operationOperations per unit time
Matters forInteractive requests, user-facing pathsBatch jobs, pipelines, background work
Improved byRemoving work from the critical path, parallelism within one requestBatching, pipelining, better resource utilisation
Typical unitp50, p95, p99 in millisecondsRequests or records per second

The two genuinely conflict. Batching improves throughput by amortising fixed costs over many items, and it worsens latency because the first item waits for the batch to fill. Adding queueing smooths load and raises utilisation while making every queued request slower. A system tuned for maximum throughput is frequently a system with terrible tail latency, and the reverse holds too.

Gotcha: measure latency as a distribution, never as an average. An average hides the tail, and the tail is what users complain about. The catalogue's observability course covers why percentiles cannot be averaged and why the p99 matters more than it looks, and everything there applies directly to profiling work.

5. The loop that actually works

Performance work is a measurement loop, and skipping any step is how teams end up slower after a month of optimisation.

It starts with a goal, because without one there is no way to know when to stop and no way to judge whether a change was worth its complexity. Then a baseline: measure the current behaviour under realistic conditions, and record it somewhere durable.

Profiling identifies where time actually goes, and the arithmetic from earlier in this lesson decides which of those places is worth attacking. Only then does a change get made, and it must be one change, because two simultaneous changes cannot be attributed.

Measuring again either confirms an improvement or does not, and the honest response to "no improvement" is to revert. An optimisation that did not help is not neutral: it is complexity added for nothing, and it will be maintained for years by people who assume it was necessary.

Key idea: the loop's discipline is that every change is a hypothesis with a measurement attached. Without the before-and-after pair, you have not optimised anything, you have merely changed the code and formed an opinion.

flowchart TD
A["Set a target: what is fast enough?"] --> B["Measure a baseline under realistic load"]
B --> C["Profile: where does the time go?"]
C --> D["Apply Amdahl: which part is worth it?"]
D --> E["Change exactly one thing"]
E --> F["Measure again"]
F --> G["Improved: keep it, update the baseline"]
F --> H["Not improved: revert it"]
G --> I["Target met? Stop."]
H --> C

6. Setting a target that means something

"Faster" is not a goal, because it has no end. A usable target has three parts, and writing them down before starting is what prevents optimisation from becoming an open-ended activity.

  • A metric with a percentile. Not "the search endpoint", but "the p95 of the search endpoint".
  • A number and a condition. Under 300 ms, at the peak traffic we actually see, with production-shaped data.
  • A reason. Because the SLO says so, because conversion drops beyond that point, because the upstream timeout is 500 ms and we need headroom.

The third part is the one usually missing, and it is what makes the target defensible when someone asks why not 200 ms.

In practice: performance work has sharply diminishing returns, and a target is what lets you notice you have reached them. The first afternoon typically finds something large and obvious; the second week is fighting for percentages against code that is now harder to read. Without a stated target, nothing signals when the trade stopped being worth it, and the codebase quietly accumulates cleverness nobody can safely modify.

With the goal set and the arithmetic understood, the question becomes how to find where the time is actually going, which is what a profiler is for.

7. The cases where you should not profile first

The measure-first rule has real exceptions, and knowing them prevents the discipline from becoming a ritual.

Algorithmic complexity is a design decision, not an optimisation. If a lookup is a linear scan through a list that will hold a million entries, you do not need a profiler to know it belongs in a hash map. Choosing the right data structure up front costs nothing and is not premature.

Known-expensive operations in a loop. A network call, a database query or a file read inside an iteration is a defect whether or not it currently shows in a profile, because the profile reflects today's data volume.

Obvious waste. Work computed and discarded, results recomputed instead of held, debug logging left at full verbosity in a hot path.

Gotcha: Knuth's remark about premature optimisation being the root of all evil is routinely used to justify shipping an accidentally quadratic algorithm. It was never an argument against thinking, only against micro-tuning code whose cost nobody has established. Choosing an appropriate algorithm is design; hand-unrolling a loop before measuring is the thing being warned about.

The honest boundary is whether you know the cost or are guessing at it. Known-bad patterns can be fixed on sight. Everything else needs evidence, and the next lesson is about how that evidence is produced.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Why is developer intuition about performance structurally unreliable?
    • Profilers are too complex for most developers to interpret
    • Source code shows structure rather than duration, so a cheap call and an expensive one look identical, and attention follows complexity rather than cost
    • Modern compilers reorder code unpredictably
    • Performance varies too much between runs to reason about
  2. A component is 10% of total runtime. What is the maximum possible whole-system speedup from optimising it?
    • About 1.11x, since 1/(1-p) caps it even if the component becomes instantaneous
    • 10x, matching its share of runtime
    • Unbounded, depending on the optimisation
    • About 1.9x
  3. A request is 600 ms database wait, 150 ms serialisation, 50 ms business logic. A 10x speedup of the business logic yields what?
    • About 2x faster overall
    • About 1.5x faster overall
    • About 1.06x faster overall: 800 ms becomes 755 ms
    • No measurable change, since 50 ms is within noise
  4. Why do latency and throughput optimisations often conflict?
    • They are measured with different profiling tools
    • Throughput work requires more memory than latency work
    • Latency can only be improved in compiled languages
    • Batching and queueing raise throughput by amortising fixed costs and increasing utilisation, while making individual operations wait longer
  5. Which case legitimately does NOT require profiling first?
    • A linear scan used as a lookup over data that will grow to a million entries
    • A function that looks computationally complex
    • A module written in a slower language than the rest of the system
    • Code that has not been reviewed for efficiency

Related lessons

Programming
intermediate

Benchmarks That Hold Up, and Knowing When to Stop

A benchmark is an experiment, and most are badly designed enough to produce confident wrong answers. This lesson covers what a measurement must control to mean anything, the ways microbenchmarks lie including code the compiler deletes, how to catch regressions in continuous integration despite noisy machines, and how to recognise the point where optimising stops paying.

7 steps·~11 min
Programming
intermediate

Where Time Actually Goes: The Six Usual Suspects

Slow software is slow for a short list of reasons, and each one has a signature you can recognise before you find the code. This lesson covers the six recurring bottleneck classes, waiting on I/O, chatty queries, allocation pressure, lock contention, memory access patterns and serialisation, with the symptom that identifies each and the fix that actually works.

7 steps·~11 min
Programming
intermediate

How Profilers Work, and How to Read a Flame Graph

A profiler is not a neutral observer: sampling and instrumentation see different things, distort the program in different ways, and answer different questions. This lesson covers how each works, why CPU time and wall-clock time give opposite answers, and how to read a flame graph correctly, including the axis that means nothing and is misread constantly.

7 steps·~11 min
Math
advanced

Newton's method and the interior point revolution

Second derivatives buy something gradients cannot: a step shaped by curvature, immune to conditioning, converging quadratically. This lesson builds Newton's method, then layers it on a log barrier to get interior point methods, the machinery that made large constrained problems solvable with a certificate rather than a hope.

13 steps·~20 min