Phase 17 - Lesson 17.5

Performance: Profiling, Vectorization, and Introductory C++ Concepts

Measure before you optimize. Then vectorize the hot path - and understand why a compiled, statically-typed language like C++ is faster when you finally need it.

⏱ 55 min● Advanced🔗 Prereqs: 17.1
↖ Phase 17 hub
Builds on: 17.1 introduced vectorization; here you decide where speed matters and how to get it.
Leads to: Latency-sensitive execution and market-making (Phase 16, capstones 6–7) rely on the performance mindset here.

Learning Objectives

Click a status chip to cycle: Not started → In progress → Studied → Practiced → Needs review → Mastered.

Key Vocabulary

Profiling
Measuring where a program actually spends time (per function/line) rather than guessing.
Hot path
The small fraction of code that consumes most of the runtime; the only place optimization pays.
Amdahl’s law
The bound on total speedup when you accelerate only part of a program: limited by the un-accelerated fraction.
Static typing
Types fixed and known at compile time (as in C++), enabling the compiler to emit specialized machine code.
Compilation
Translating source to machine code ahead of run time, so no interpreter overhead is paid per operation.
Cache locality
Data laid out contiguously so the CPU cache is used well; a major reason contiguous arrays beat pointer-chasing.

Intuition & Motivation

Intuition
The first rule of performance is: measure, don’t guess. Programmers are notoriously wrong about where time goes; the slow line is rarely the one you suspect. A profiler tells you the truth, and the truth is usually that 90% of the time sits in 5% of the code - the hot path. Optimizing anything else is wasted effort.

Once you know the hot path, the Python answer is usually vectorization (17.1): move the inner loop into compiled C. When even that isn’t enough - a genuinely serial, latency-critical inner loop - you drop to a compiled, statically-typed language. Understanding why C++ is faster (no interpreter, fixed types, cache-friendly layout) tells you when the jump is worth its considerable correctness and complexity cost.

Measure first: profiling and Amdahl’s law

A profiler attributes runtime to functions or lines. Before touching code, you profile, find the hot path, and estimate the payoff. If a section takes fraction \(p\) of total time and you speed it up by factor \(s\), the overall speedup is bounded by Amdahl’s law:

\[\text{speedup} = \frac{1}{(1-p) + p/s}\;\le\;\frac{1}{1-p}.\] (17.6)

If the hot path is 90% of runtime (\(p=0.9\)) and you make it infinitely fast (\(s\to\infty\)), you can at most get \(1/(1-0.9)=10\times\). If it’s only 20% of runtime, no amount of optimization beats \(1.25\times\). This is why you optimize the hot path and ignore the rest.

Worked Example - Reading a profile and choosing the target
1
Profile a backtest: 82% of time in compute_signal (a Python loop over dates), 9% in I/O, 9% in plotting.
2
Amdahl bound from vectorizing the signal (\(p=0.82\)): at best \(1/(1-0.82)\approx5.6\times\) overall - worth it.
3
Optimizing plotting (\(p=0.09\)): at best \(1/0.91\approx1.10\times\) - not worth the effort.
4
So: vectorize compute_signal; leave plotting alone. Re-profile after - the hot path often moves.

Vectorization as the first-line optimization

For array math, a vectorized rewrite typically buys 50–200× on the hot loop for near-zero risk, because it is the same algorithm expressed as whole-array operations (17.1). Always try this before reaching for another language: it is faster to write, easier to test, and stays in one codebase.

Why C++ is faster: types, compilation, layout

Python is interpreted and dynamically typed. Every a + b asks at run time: what types are these? where are their methods? - then boxes the result as an object. C++ is compiled and statically typed: the compiler already knows a and b are doubles and emits a single machine instruction. Three concrete reasons a C++ inner loop outruns a Python one:

  1. No interpreter overhead. Machine code runs directly; there is no per-operation bytecode dispatch or type check.
  2. Static types. Knowing everything is a double lets the compiler specialize and use CPU vector (SIMD) instructions.
  3. Cache-friendly memory. A std::vector<double> is contiguous, like a NumPy array, so the CPU cache is used well instead of chasing pointers to boxed Python objects.
TraitPython (CPython)C++
Typingdynamic, checked at run timestatic, checked at compile time
Executioninterpreted bytecodecompiled machine code
Numeric loopboxed objects, interpreter overheadraw doubles, SIMD-able
Dev speed / safetyfast to write, memory-managedslower, manual memory, easy to crash
Key Idea
NumPy gives you most of C++’s numeric speed from Python, because its inner loops are already compiled C over contiguous typed arrays. You reach for actual C++/Numba/Cython only for genuinely serial, latency-critical inner loops that cannot be vectorized - e.g. an order-book event loop.
Common Mistakes to Avoid
  • Optimizing before profiling - you speed up the 9% and the program is still slow (Amdahl).
  • Rewriting in C++ when a one-line vectorization would have given the same speedup with far less risk.
  • Micro-optimizing readability away on the cold path, where it buys nothing and costs clarity.
  • Assuming ‘C++ is always faster’ while calling it in tiny chunks - the Python↔C boundary crossing dominates.
  • Trading correctness for speed: manual memory management and undefined behavior in C++ introduce bugs vectorized Python never had.
  • Forgetting to re-profile after an optimization; the hot path moves and your next guess is again wrong.
Quant Practitioner Tips
  • Profile, optimize the hot path, re-profile. Repeat until the profile is flat or fast enough.
  • Use Amdahl’s law to decide if an optimization is even worth attempting before you start.
  • Vectorize first (NumPy). Consider Numba/Cython next; reach for C++ only for serial latency-critical loops.
  • When you do go to compiled code, keep the pure-Python version as an oracle and test them against each other (17.2).
  • Measure wall-clock on realistic input sizes; asymptotic wins can lose to constant factors at small N.

Interactive: profile-motivated vectorization with a speedup check

The starter has a slow loop (the hot path) and asks for a vectorized version that returns identical numbers. The tests check equivalence; conceptually, this is the \(50\text{-}200\times\) win the profile predicts.

Knowledge Check

Q1 Medium
The hot path of your program is 25% of runtime. The best possible overall speedup from optimizing only it is:
\(4\times\)
\(1.33\times\)
\(\infty\)
\(1.25\times\)
Q2 Medium
A C++ numeric inner loop typically beats the Python equivalent mainly because:
C++ has better libraries
It is compiled to machine code with static types, avoiding per-operation interpreter overhead and enabling SIMD
Python cannot do arithmetic
C++ uses the GPU
Q3 Easy
Before optimizing quant research code, the correct first step is to:
Rewrite the slowest-looking function in C++
Profile to find where time is actually spent
Add more cores
Delete the tests to save time

Practical Exercise

A pricing library spends, per a profile, 70% of its time in a Monte Carlo path loop, 20% in payoff evaluation, and 10% in reporting. (a) You can vectorize the path loop for a 100× speedup on that section. What is the overall speedup by Amdahl’s law? (b) After that change, re-profile in words: which section is now the new hot path? (c) When, if ever, would you rewrite the path loop in C++ instead of vectorizing?

▶ Show full solution

(a) \(p=0.70,\ s=100\): overall \(=1/((1-0.70)+0.70/100)=1/(0.30+0.007)=1/0.307\approx3.26\times\).

(b) Originally path=70, payoff=20, report=10 (relative). After the 100× win the path costs \(0.70/100=0.007\), so relative shares become payoff \(0.20\), report \(0.10\), path \(0.007\) - total \(0.307\). The payoff evaluation (\(0.20/0.307\approx65\%\)) is now the hot path; re-profiling and vectorizing it is the next move.

(c) Rewrite the path loop in C++ (or Numba) only if it is genuinely serial and cannot be vectorized (e.g. path-dependent early exercise with per-step branching), it dominates runtime, and the vectorized/NumPy version is still too slow for the latency budget - and even then keep the Python version as a correctness oracle.

After the reveal, answer for yourself: How does Amdahl’s law change your intuition about ‘just rewrite it all in C++’?

Lesson Summary

Performance work is disciplined: profile to find the hot path, use Amdahl’s law \(1/((1-p)+p/s)\) to bound the payoff, and optimize only what matters. In Python the first tool is vectorization (compiled C inner loops). A statically-typed, compiled language like C++ is faster because it drops interpreter overhead, specializes on fixed types (SIMD), and uses cache-friendly contiguous memory - but you pay in complexity and correctness risk, so reach for it only for serial, latency-critical loops.

Retrieval Practice

Close the lesson and answer from memory before checking. This is deliberate, effortful recall - the single highest-yield study action.

▶ Show retrieval prompts & answers
Q: State Amdahl’s law and its limiting speedup.
A: \(\text{speedup}=1/((1-p)+p/s)\), bounded above by \(1/(1-p)\) as \(s\to\infty\), where \(p\) is the time fraction in the optimized section.
Q: Give two reasons a compiled statically-typed C++ loop outruns an interpreted Python loop.
A: No per-operation interpreter overhead (machine code runs directly) and static types on unboxed values let the compiler specialize and use SIMD; contiguous memory also improves cache locality.

Completion Checklist

Confidence / mastery rating
Personal notes

Source References

This lesson synthesizes and paraphrases concepts from the sources below. No copyrighted text, problem sets, or solutions are reproduced. Return to the originals for full depth.