Profiling

By the end of this lesson you'll be able to time C++ code accurately with std::chrono , avoid the traps that make micro-benchmarks lie, reach for the right profiler to find real hotspots, and reason about whether an optimisation is even worth doing — so you optimise what matters instead of guessing.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Optimising without profiling is like a doctor prescribing surgery before running any tests. You feel the problem is the heart, so you operate on the heart — when the real issue was a vitamin deficiency. A profiler is the diagnostic scan : it shows you exactly which part of the body (your code) is consuming the time. Only after the scan do you pick the treatment. Programmers who skip the scan spend days "fixing" a function that runs for 0.1% of the total time, while the real culprit sits untouched. Measure first, then operate.

1. Measure, Don't Guess — Timing with std::chrono

The single most expensive habit in performance work is guessing . Your intuition about which line is slow is almost always wrong, because the CPU's caches, branch predictor, and the optimiser reshape your code in ways you can't see. The cure is to measure . The simplest tool is std::chrono : take a timestamp before the work, another after, and subtract. high_resolution_clock is the finest-grained clock the standard library offers — perfect for small benchmarks.

The pattern is always the same: auto start = Clock::now(); → do the work → auto end = Clock::now(); → duration_cast the gap into the unit you want (microseconds here). Read the worked example, run it, and notice it checks both answers match before comparing speed — a fast wrong answer is worthless.

Your turn. The program below times how long it takes to build a big vector — fill in the two timestamp blanks and the conversion, then run it.

2. Micro-Benchmark Pitfalls

A micro-benchmark times a tiny isolated snippet — and tiny snippets lie more than any other kind of measurement. The most infamous trap: if you compute a result and never use it, an optimising compiler is allowed to delete the entire calculation . Your benchmark then reports near-zero time and you celebrate a speed-up that never happened. The fix is to make the result observable — print it, or store it in a volatile so the compiler must actually do the work.

3. Profilers — Find the Real Hotspot

chrono answers "how long does this take?". A profiler answers the more important question: "across my whole program, where is the time going?". It runs your program and reports a breakdown by function. A hotspot is a function with a large share of the total — that's where optimisation pays off. There are two flavours: sampling profilers (like perf ) interrupt the program thousands of times a second and record where it is, giving low overhead and realistic numbers; instrumenting profilers (like Callgrind) count every call exactly, giving precise call graphs but running much slower.

perf (Linux): the go-to sampling profiler. perf record ./app then perf report shows a sorted list of hotspots with almost no slowdown. First choice for real workloads.

Valgrind / Callgrind: valgrind --tool=callgrind ./app gives an exact call graph and per-line instruction counts. Visualise it with kcachegrind . Slow (10–50×) but deterministic — great for understanding why a function is hot.

gprof: the classic. Compile with g++ -pg , run, then gprof a.out gmon.out . Simple function-level timings; dated, but everywhere.

Google Benchmark: a micro -benchmark library, not a profiler. It runs each case enough times for stable numbers, handles warm-up, and offers benchmark::DoNotOptimize(x) to defeat exactly the "optimised away" trap from Section 2. Reach for it when you're comparing two implementations carefully.

Whatever the tool, reading a profile is the same skill: look at the top of the sorted list, ignore the long tail. A function eating 60% of the run time is your target; a hundred functions at 0.2% each are not worth touching. This is where your effort multiplies.

4. Is It Even Worth It? Amdahl's Law & Big-O

Before you optimise, ask what the payoff can be. Amdahl's law says your overall speed-up is capped by the part you don't improve. If a function is 90% of the run time, making it infinitely fast still only makes the whole program 10× faster — and optimising a 10% slice can never beat 1.11× no matter what. This is the maths behind "optimise the biggest slice the profiler shows you."

The other lens is big-O vs constant factors . Big-O tells you how cost grows as the input grows — O(n) doubles when the input doubles, O(n²) quadruples. It is the most reliable performance lever you have, because a better big-O wins by ever-larger margins as data scales. But big-O hides the constant factor : for small inputs a "worse" algorithm with a tiny constant can win. Below, replace the blank so you can watch an O(1) hash lookup demolish an O(n) linear scan as soon as the data is non-trivial.

No blanks this time — just a brief and a blank canvas (with an outline to keep you on track). Time two ways of building a big string, then prove to yourself which one wins and by how much. Remember to print a result so the compiler can't delete your benchmark.

Practice quiz

What is the golden rule of performance work in this lesson?

  • Always rewrite loops in assembly
  • Optimize every function equally
  • Measure, don't guess
  • Use the newest compiler

Answer: Measure, don't guess. Intuition about what's slow is usually wrong; measure first so you optimize what actually matters.

Which standard facility times a snippet by taking timestamps before and after?

  • std::chrono (e.g. high_resolution_clock::now())
  • std::clock_t only
  • std::timer
  • std::profile

Answer: std::chrono (e.g. high_resolution_clock::now()). std::chrono with Clock::now() before and after, then duration_cast on the gap, is the simple timing tool.

What is the difference between profiling and benchmarking?

  • They are the same thing
  • Profiling times one snippet; benchmarking maps the whole program
  • Benchmarking only works on Linux
  • Benchmarking times a specific piece of code; profiling shows where a whole program spends its time

Answer: Benchmarking times a specific piece of code; profiling shows where a whole program spends its time. You benchmark a candidate fix ('how fast?'); you profile to find which part to fix ('what's slow?').

Why might a micro-benchmark report ~0 microseconds for real work?

  • The clock is broken
  • If the result is never used, the optimizer is allowed to delete the whole calculation
  • Microseconds are always zero
  • The loop ran backward

Answer: If the result is never used, the optimizer is allowed to delete the whole calculation. An unused result lets the compiler elide the computation; make the result observable (print it or use volatile).

How do you stop the compiler from optimizing away timed work?

  • Make the result observable — print it, store it in a volatile, or use a do-not-optimize sink
  • Disable the timer
  • Run the loop fewer times
  • Compile with -O0

Answer: Make the result observable — print it, store it in a volatile, or use a do-not-optimize sink. Forcing the result to be observed (print/volatile/DoNotOptimize) prevents the optimizer from eliding the work.

Why do timing numbers jump around between runs?

  • It always means a bug
  • chrono is unreliable
  • OS scheduling, CPU frequency scaling, and a cold cache add noise — run several times and report the median or minimum
  • Only because of memory leaks

Answer: OS scheduling, CPU frequency scaling, and a cold cache add noise — run several times and report the median or minimum. Variation is normal; warm up the cache, run multiple times, and report the median (or minimum) rather than one run.

What is the difference between a sampling profiler (like perf) and an instrumenting profiler (like Callgrind)?

  • Sampling counts every call exactly; instrumenting interrupts periodically
  • Sampling interrupts periodically with low overhead; instrumenting counts every call exactly but runs much slower
  • They are identical
  • Only instrumenting profilers work on real workloads

Answer: Sampling interrupts periodically with low overhead; instrumenting counts every call exactly but runs much slower. perf samples thousands of times a second (low overhead); Callgrind counts every call exactly but is much slower.

When reading a profile, where should you focus your effort?

  • The bottom of the sorted list
  • Every function equally
  • Alphabetically by function name
  • The top of the sorted list (the biggest hotspots); ignore the long tail

Answer: The top of the sorted list (the biggest hotspots); ignore the long tail. A function eating 60% of run time is the target; a hundred functions at 0.2% each aren't worth touching.

Per Amdahl's law, if a function is 90% of run time, making it infinitely fast speeds up the whole program by at most how much?

  • 2x
  • 10x
  • 100x
  • Unlimited

Answer: 10x. The untouched 10% caps the gain: 1 / (1 - 0.90) = 10x. amdahl(0.90, infinity) approaches 10.

Should you always pick the algorithm with the lower big-O?

  • Yes, always, regardless of input size
  • No, big-O never matters
  • Usually, but not blindly — big-O hides the constant factor, so a worse big-O can win for small inputs
  • Only for sorting

Answer: Usually, but not blindly — big-O hides the constant factor, so a worse big-O can win for small inputs. Big-O wins as data scales, but for small inputs a tiny-constant O(n^2) can beat a big-constant O(n log n). Measure at real size.