C++ Interview Prep · Part 3

Performance & Optimization

For optimization-focused roles: the memory hierarchy, data layout, branches, vectorization, allocation, compiler behavior, and — above all — measurement. Traps ⚠️, one-liners 💬.

⚠️ interview trap💬 one-liner worth saying verbatim

01The Mental Model: Latency Numbers

Modern optimization is mostly about one fact: CPUs got fast; memory didn't keep up. A core can execute 4+ instructions per cycle at ~4–5 GHz, but a trip to main memory stalls it for hundreds of instruction slots. Approximate numbers to carry in your head (they anchor every argument you'll make in an interview):

OperationLatency≈ scaled to human time (1 cycle = 1 s)
Register / simple ALU op~0.25–1 nsseconds
L1 cache hit~1 ns (4–5 cycles)seconds
L2 cache hit~4 ns (~14 cycles)~15 s
L3 cache hit~15–40 nsa minute or two
Main memory (RAM)~60–100 nsminutes
Branch misprediction~15–20 cycles~20 s
Mutex lock/unlock, uncontended~20–50 nsa minute
NVMe SSD read~20–100 µsdays
Same-datacenter network round trip~500 µsa week

Numbers vary by hardware — what matters is the ratios: RAM is ~100× L1; disk is ~1000× RAM. One cache miss costs the same as dozens of arithmetic operations, so an algorithm that does more work on resident data routinely beats one that does less work with scattered access. That single inversion of intuition explains most of this guide.

💬 One-liner"Big-O counts operations; hardware charges for memory. When they disagree at real sizes, memory usually wins."

↑ back to top

02Cache Mechanics

Lines, hierarchy, locality

⚠️ Trap — the linked list question

"List has O(1) insert, vector has O(n) — when is the vector still faster?" Almost always at realistic sizes: traversing to the insertion point costs a cache miss per node (~100 ns each), while the vector's shift is a sequential memmove streaming at GB/s with the prefetcher's help. Vectors also have no per-node allocation or 16-byte pointer overhead. Real answer interviewers want: "measured crossover points are surprisingly large; default to vector, use a list only when iterator stability or splicing is the requirement."

Row-major traversal

// C++ 2D arrays are row-major: m[i][j] and m[i][j+1] are adjacent bytes.
for (i...) for (j...) sum += m[i][j];   // sequential: ~1 miss per 16 ints
for (j...) for (i...) sum += m[i][j];   // stride = row length: miss per ACCESS
// Same operation count. 5–20x runtime difference at sizes past L2.

The grown-up version of this answer is loop tiling/blocking: process the data in sub-blocks that fit in cache (the classic matrix-multiply optimization). Name it; deriving it live is rarely required.

💬 One-liners"Memory arrives 64 bytes at a time — the question is always how many of those bytes I actually use."

"The prefetcher hides latency for predictable strides; pointer chasing makes every hop a full-price miss."

↑ back to top

03Data Layout

AoS vs SoA vs pointer soup

Three ways to store N objects, ranked by cache behavior for field-wise processing (AoS = array of structures; SoA = structure of arrays):

// AoS — array of structures: whole objects contiguous
struct Particle { float x, y, z, mass; };
std::vector<Particle> ps;

// SoA — structure of arrays: one contiguous array per field
struct Particles { std::vector<float> x, y, z, mass; };

// pointer soup — contiguous pointers, scattered objects (worst)
std::vector<std::unique_ptr<Particle>> pps;

Padding, alignment, packing

struct Bad  { char c; double d; char c2; };  // 24 bytes (7 + 6 bytes padding)
struct Good { double d; char c, c2;   };  // 16 bytes — just reordered
💬 One-liner"Layout is the highest-leverage optimization because it changes what every loop pays, without touching the loops."

↑ back to top

04Object Size: sizeof, Alignment, Padding

Object size is the numerator of every cache argument: 64-byte lines ÷ sizeof(T) = objects per line, so shrinking a struct from 24 to 16 bytes is a 33% cut in memory traffic for every loop that touches it — without changing a single loop. Being able to compute sizeof by hand is table stakes for an optimization role.

The layout algorithm (compute it by hand)

struct Order {          // alignment 8 (double)
    bool   urgent;      // offset 0
                        // 7 bytes padding (price needs offset % 8 == 0)
    double price;       // offset 8
    int    qty;         // offset 16
    bool   flagged;     // offset 20
};                      // 21 -> tail-pad to 24.  sizeof = 24

struct Order2 {         // same members, descending alignment
    double price;       // 0
    int    qty;         // 8
    bool   urgent;      // 12
    bool   flagged;     // 13
};                      // pad to 16.  sizeof = 16 — 33% smaller, free

The odd cases (favorite follow-ups)

⚠️ Trap — #pragma pack is not a size optimization

Packing removes padding by allowing misaligned members. Costs: slower (sometimes split) loads on x86, faults on some ARM configurations, broken atomics, and taking a pointer/reference to a misaligned member is a portability landmine. Legitimate use: matching an on-disk or on-wire format — and even then, prefer memcpy into a properly aligned struct at the boundary. If asked "how do I shrink this struct," the answer is reorder, split hot/cold, bitfield — not pack.

Typical sizes worth knowing cold (x86-64, Itanium ABI)

TypeTypical sizeWhy
T*, reference, unique_ptr<T>8one pointer; stateless deleter via EBO
shared_ptr / weak_ptr16object pointer + control-block pointer
std::vector24begin / end / capacity pointers
std::string32 (libstdc++) / 24 (libc++)SSO buffer folded into the object
std::optional<double>16payload + bool, padded to payload's alignment
std::variant<int, double>16largest alternative + discriminant + padding
std::mutex40 (Linux/glibc)wraps pthread_mutex_t — why per-object mutexes bloat
std::function32–64 (varies)type erasure + small-callable buffer

Exact numbers vary by standard library and platform — quote them as "typical on 64-bit with the Itanium ABI (application binary interface)" and you're both useful and safe.

Tooling

💬 One-liners"sizeof is an audit: members in declaration order, padding to each one's alignment, tail-pad for arrays, plus a vptr if polymorphic — nothing else."

"The compiler can't reorder members, so ordering by descending alignment is a human's job — and it's free bandwidth."

↑ back to top

05Branches & Speculation

The famous demo

// sum only elements >= 128, over random bytes:
for (int x : data) if (x >= 128) sum += x;
// SORTED data: ~5x faster than unsorted. Same elements, same total work —
// sorted input makes the branch predictable (all-false then all-true).

Going branchless

sum += (x >= 128) ? x : 0;        // often compiles to cmov / arithmetic — no branch
int mx = std::max(a, b);           // compilers emit cmov for simple selects
mask = -(x >= 128); sum += x & mask;  // manual mask trick, same idea
💬 One-liner"Branches are free when predictable and ~20 cycles when not — so the question is never 'how many branches' but 'how predictable.'"

↑ back to top

06ILP & Vectorization

ILP — instruction-level parallelism

// One accumulator: every add waits on the previous one (dependency chain)
for (i) sum += a[i];

// Four accumulators: four independent chains run in parallel on one core
for (i += 4) { s0 += a[i]; s1 += a[i+1]; s2 += a[i+2]; s3 += a[i+3]; }
sum = s0 + s1 + s2 + s3;   // often 2–4x, no threads involved

A modern core is superscalar (executes several instructions per cycle) and out-of-order (runs whatever's ready). What limits it is dependency chains — each instruction waiting on the last one's result. Breaking a reduction into independent accumulators is the canonical fix. (Compilers do this for integer sums; for floats they need -ffast-math-style permission, because FP addition isn't associative.) ⚠️

SIMD — single instruction, multiple data

💬 One-liners"A single core is already parallel twice over — multiple ports and vector lanes — and dependency chains are what waste it."

"The vectorizer eats simple loops over contiguous data; my job is mostly to stop feeding it excuses."

↑ back to top

07Allocation Costs

💬 One-liner"Allocation costs twice: once at the call, and forever after in where the data ended up."

↑ back to top

08What the Compiler Does For You

💬 One-liner"My first optimization is making the code inlineable and alias-free — the compiler does the rest better than I do."

↑ back to top

09Concurrency Performance

struct Counters {
    alignas(64) std::atomic<long> a;   // each on its own cache line
    alignas(64) std::atomic<long> b;
};
// std::hardware_destructive_interference_size = the portable "64"
💬 One-liners"False sharing is a performance data race: invisible to correctness tools, obvious in perf counters."

"Contended anything is slow — the win is sharing less, not locking cleverer."

↑ back to top

10Measurement — the Real Skill

Everything above is hypothesis-generation. Measurement is the actual job, and interviewers for optimization roles probe it harder than any mechanism. The discipline:

  1. Profile before touching anything. Intuition about where time goes is notoriously wrong. Tools: perf (Linux — perf record/perf report, perf stat for counters like cache-misses, branch-misses, IPC), Intel VTune, Instruments (macOS), flame graphs for visualization.
  2. Find the hot spot, form a hypothesis from hardware counters: high cache-miss rate → layout/access problem (§2–3); high branch-miss rate → §4; low IPC (instructions per cycle) with low misses → dependency chains (§5); time in malloc → §6.
  3. Change one thing, measure again, keep or revert. Optimization without a before/after number is superstition.

Microbenchmark honesty

// Google Benchmark skeleton — the standard tool
static void BM_Sum(benchmark::State& st) {
    auto v = make_data();
    for (auto _ : st) {
        auto s = sum(v);
        benchmark::DoNotOptimize(s);      // stop the as-if rule deleting the loop
    }
}
💬 One-liners"Profile first — the hot spot is never where you think, and I have the scars to prove it."

"An optimization without a before/after measurement is a rumor."

↑ back to top

11Classic "Why Is This Slow?" Questions

Symptom / questionAnswer sketch
Column-wise 2D loop slowRow-major layout → stride = row length → miss per access. Swap loop order; tile if both orders are needed.
Same loop, sorted input 5× fasterBranch predictor. Sorted → predictable. Fix for random: branchless select.
vector<unique_ptr<Base>> hot loopPointer chase per element + indirect call. Per-type vectors, variant, or sort by type.
Threads made it slowerFalse sharing (pad/alignas), contention (shard), or tasks too small (batch). Check with perf counters.
Slower after "removing work" with a helper in another .cppKilled inlining across the translation unit boundary. Header/inline it or enable LTO.
map vs unordered_map vs sorted vectorunordered_map O(1) but hashing + a pointer chase per bucket; map = pointer chase per level; sorted vector + lower_bound is often fastest to iterate/search at moderate sizes — contiguity beats asymptotics. Measure.
Loop won't vectorizeAliasing, non-unit stride, or a data-dependent branch. __restrict, SoA, hoist the branch; verify with -Rpass/-fopt-info.
String-heavy code slowAllocations (reserve, SSO awareness, string_view), and hashing costs in unordered containers.

Answer pattern for all of them: name the hardware mechanism → state the fix → say how you'd confirm with a measurement. Mechanism–fix–measurement is the three-beat answer that reads as "has done this professionally."

↑ back to top

12Cheat Sheet

↑ back to top