High Performance

By the end of this lesson you'll be able to make C++ run several times faster the right way: measure first, lay your data out so the CPU cache loves it, stop copying things you don't need to, and turn on the compiler optimisations that do the heavy lifting for you.

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.

Think of the CPU as a chef and memory as a giant warehouse. The chef keeps a tiny counter (the cache) right next to the stove. When ingredients sit together on one shelf (a contiguous std::vector ), an assistant grabs a whole tray at once and the chef never waits. When ingredients are scattered all over the warehouse (a node-based std::list ), the assistant runs back and forth for each one — that round trip to RAM is roughly 100× slower than reaching the counter. Most "slow" C++ isn't slow because of clever maths; it's slow because the chef is standing around waiting for memory. Performance work is mostly about keeping the right things on the counter, and about not copying trays you only need to read .

1. Measure First — Never Guess

The golden rule of optimisation is measure, don't guess . Programmers are notoriously bad at predicting where time goes — most of a program's runtime hides in a tiny slice of the code, and the rest is irrelevant. So before you change anything, put a clock around the work and get a real number. A timer is all you need to compare two versions of the same task.

Once you can measure, you can optimise honestly: change one thing, re-run, and keep the change only if the number actually drops. Everything below follows the same pattern — a slow "before" and a fast "after", with a timer proving the difference.

2. Cache Locality — Keep Data Contiguous

The CPU never fetches one byte at a time. It pulls a cache line — usually 64 bytes — into a small, blazing-fast cache. A std::vector stores its elements in one contiguous block , so walking it reads sequential bytes the CPU can prefetch; this is called data-oriented design — laying memory out for how you'll read it. A node-based container like std::list scatters each element somewhere different, so every step risks a cache miss — a trip to RAM that's about 100× slower. The lesson: prefer contiguous containers unless you genuinely need constant-time insertion in the middle.

3. Avoid Unnecessary Copies

A huge amount of accidental slowness comes from copying data you only meant to read . Passing a big std::vector or std::string by value duplicates the whole thing on every call. The fix is to pass large, read-only arguments by const reference ( ) — the function gets a read-only alias to the original and copies nothing. Pass small types ( int , double , a pointer) by value; pass big ones you won't modify by . This is the single highest-value habit in everyday C++ performance.

Your turn. The function below copies the whole vector — and every string inside it — on every call. Change the parameter to a and the loop item to a so nothing is copied. Fill in the two blanks marked ___ :

4. reserve() — Stop Reallocating

When you push_back into a growing std::vector , it occasionally runs out of room, allocates a bigger block, and copies every element it already holds into the new block. Do that as the vector grows from 1 to a million and you've copied a lot of data many times over. If you know roughly how many items you'll add, call vec.reserve(n) once up front: the vector grabs the whole block immediately and never re-copies. It's one line and it's basically free speed.

Now you try. The loop below knows it will add N items, so it should reserve that space first. Fill in the blank with the right call:

5. Inlining, Move & Compiler Flags

Some speed isn't yours to win by hand — it belongs to the compiler. Inlining pastes a small function's body straight into the caller, removing the call overhead; at -O2 the compiler does this for hot functions automatically, so you rarely write inline for speed. Move semantics ( std::move ) hand ownership of a big buffer across instead of copying it. And the biggest lever of all is the optimisation flag : compile a release build with -O2 and the same source can run several times faster than an unoptimised -O0 build.

6. Branch Prediction (Briefly)

Modern CPUs guess which way an if will go before they know the answer, so they can keep working. Guess right (predictable data) and the branch is nearly free; guess wrong (random data) and the CPU throws away ~15–20 cycles of work. You usually don't micro-manage this, but two practical takeaways hold: processing sorted data makes branches predictable, and a branchless expression like avoids the penalty entirely in a hot loop. As always — measure before reaching for either.

Array of Structures (AoS) groups all the fields of one object together: vector<Particle> where each Particle holds x, y, z, mass, type . It reads naturally, but if a loop only needs x , every cache line also drags in y , z , mass and type — mostly wasted.

Structure of Arrays (SoA) flips it: one array per field — vector<float> x, y, z; . Now a loop over all x values reads perfectly sequential memory, so the whole cache line is useful. SoA shines for batch processing (update every particle's position), which is why game engines and simulations lean on it.

Rule of thumb: AoS when you use most fields of one object at a time; SoA when you sweep one field across everything.

No blanks this time — just a brief and an outline. Build a word counter that takes its data by , time it with , and print the count and the microseconds. The point isn't only to make it fast — it's to prove it with a number, exactly like a real performance engineer.

Practice quiz

What is the golden rule of optimisation?

  • Always use -O3
  • Inline every function
  • Measure, don't guess
  • Rewrite hot loops in assembly first

Answer: Measure, don't guess. Programmers are bad at predicting where time goes. Put a clock around the work and get a real number before changing anything.

Why is std::vector usually faster to traverse than std::list?

  • Its elements are contiguous, so the CPU can prefetch and cache them
  • It uses less memory per element only
  • It has a smaller interface
  • It never reallocates

Answer: Its elements are contiguous, so the CPU can prefetch and cache them. A vector stores elements in one contiguous block (cache-friendly). A list scatters nodes in memory, so each step risks a cache miss ~100x slower.

How should you pass a large, read-only std::vector into a function?

  • By value
  • By raw pointer copy
  • By std::move
  • By const reference (const T&)

Answer: By const reference (const T&). Passing by const reference hands the function a read-only alias to the original, copying nothing. Passing by value duplicates the whole vector.

What does vec.reserve(n) do?

  • Inserts n default elements
  • Allocates room for n elements up front so the vector won't repeatedly reallocate
  • Shrinks the vector to n
  • Sorts the first n elements

Answer: Allocates room for n elements up front so the vector won't repeatedly reallocate. reserve(n) grabs the whole block once. Without it a growing vector reallocates and copies every element several times as it expands.

In modern C++, do you usually need to write 'inline' to make a hot function fast?

  • No — at -O2 the compiler inlines small hot functions for you
  • Yes, always
  • Only for member functions
  • Only for templates

Answer: No — at -O2 the compiler inlines small hot functions for you. Modern compilers decide what to inline. Today 'inline' is mainly about allowing a definition in a header without breaking the one-definition rule.

What is the difference between -O2 and -O3?

  • -O3 is always faster
  • -O2 disables optimisation
  • -O2 is the safe default; -O3 adds more aggressive optimisations that sometimes help and sometimes hurt
  • They are identical

Answer: -O2 is the safe default; -O3 adds more aggressive optimisations that sometimes help and sometimes hurt. -O2 turns on standard, well-tested optimisations. -O3 adds aggressive ones (extra vectorisation, unrolling) — build with -O2, then measure -O3.

What does std::move(big) do to a std::string big?

  • Copies big's buffer
  • Transfers ownership of big's buffer to the target, leaving big empty
  • Deletes big
  • Sorts big's characters

Answer: Transfers ownership of big's buffer to the target, leaving big empty. Move hands ownership across instead of copying. After string moved = std::move(big), moved holds the buffer and big.size() is 0.

In SoA (Structure of Arrays) layout, when does it shine compared to AoS?

  • When you touch all fields of one object at a time
  • When objects are never accessed
  • When using std::map
  • When a loop sweeps one field across all objects (batch processing)

Answer: When a loop sweeps one field across all objects (batch processing). SoA keeps each field in its own array, so a loop over one field reads sequential memory. AoS is better when you use most fields of one object.

What happens on a branch misprediction in a hot loop?

  • The program crashes
  • The CPU throws away ~15-20 cycles of speculative work
  • Nothing measurable
  • The branch is removed

Answer: The CPU throws away ~15-20 cycles of speculative work. Modern CPUs guess which way an if goes. A wrong guess wastes cycles. Sorted data makes branches predictable; a branchless expression avoids the penalty.

Why should you avoid benchmarking an -O0 (debug) build?

  • It uses too much memory
  • It cannot run loops
  • An unoptimised binary can be several times slower, telling you nothing about release speed
  • It always crashes

Answer: An unoptimised binary can be several times slower, telling you nothing about release speed. Timing an -O0 binary is meaningless for release performance. Always measure with -O2 (or -O3) on.