“Know how to solve every problem that has been solved.” “What I cannot create, I do not understand.” — Richard Feynman

What gets measured, what gets modeled in performance engineering

Performance Engineering

Performance engineering is two activities: measure how fast a program actually runs, then use knowledge of the hardware to make it faster. The premise underneath both is easy to forget: computing feels abstract — things just get computed — but every algorithm executes on a machine, and the machine has opinions. If you want to compute on an abacus, you must represent your math the way an abacus represents numbers; a CPU is no different, it just hides its beads (caches, pipelines, branch predictors) better. The Understand the CPU pages do the measuring: cache-size detection, branch-misprediction experiments, memory-bandwidth tests. This page supplies the other half: the models that predict what those measurements should say — and what to change when a program comes in slower than its prediction.

What you measure

Four broad classes of measurement, distinguished by what instrument produces them and what they tell you about:

1. Time and throughput

Wall-clock time (with proper warm-up + statistical confidence intervals — naive single-run timings are nearly useless). Throughput in ops/sec, GB/s, or instructions per cycle (IPC). Latency distributions including the tails (p50, p99, p99.9 — averages hide the bad cases). Done with carefully-written benchmarks (Google Benchmark, Criterion.rs), Linux perf stat, or simple with repetition.

2. Hardware performance counters

The CPU has dedicated counters for everything that costs cycles. perf (Linux), Intel VTune, AMD uProf, Apple Instruments, and LIKWID read them: L1/L2/L3 cache misses, branch mispredictions, TLB misses, retired instructions, front-end stalls, back-end stalls, memory traffic, FLOPs issued, vector-unit utilization. The measurement methodology page covers the discipline; the cache-size detection and branch experiments are example readouts.

3. Scaling behavior

Strong scaling: keep the problem size fixed, vary the number of cores or nodes, measure speedup. Weak scaling: scale problem size with cores, measure how time per worker grows. The shape of the scaling curve diagnoses bottleneck type — flat from the start means serial-bound; curling over at 16 cores means contention or memory-bandwidth saturation; retrograde (slower at high core counts) means coherency traffic dominating.

4. Resource utilization

Memory footprint (RSS / VSS), heap allocation rate, file I/O rate, network bandwidth, GPU utilization, power draw. Each can be a hidden bottleneck even when CPU utilization looks fine.

What a model predicts

Performance modeling is built on a small set of foundational laws. Each predicts an observable that you compare to what your measurement actually says.

The roofline model (Williams-Waterman-Patterson 2009)

The single most useful framework. Plot performance (GFLOPS) on the y-axis against operational intensity (FLOPs/byte) on the x-axis. The achievable performance is bounded by two roofs:

where is the CPU's peak floating-point rate (clock × cores × SIMD lanes × FMA-factor-of-2) and is the achievable memory bandwidth (STREAM Triad gives this empirically). The ridge point where the two roofs intersect, at intensity , separates memory-bound from compute-bound kernels. Knowing this single number per machine is worth more than any list of micro-benchmarks.

Amdahl's law (1967)

where is the parallelizable fraction. If 10% of your code is inherently serial, you can never go faster than 10× no matter how many cores. The strong-scaling ceiling. Brutal and universal.

Gustafson's law (1988)

The weak-scaling counterpart: if you scale the problem size with , the parallel fraction stays effectively fixed and speedup is closer to linear. The argument against Amdahl's pessimism: in practice, bigger machines run bigger problems.

USL: Universal Scaling Law (Gunther)

Amdahl with two extra knobs: is contention (serialization on a shared resource) and is coherency delay (cross-thread synchronization cost). Crucially, means scaling goes retrograde — adding cores past some optimum makes things slower. Almost every real distributed system has nonzero . Fit your scaling curve to USL; if is large, coherency is your problem.

Little's law

Concurrency = throughput × latency. Universal — applies to a CPU pipeline, a network queue, a coffee shop. If you know any two, you know the third. For a CPU with 4-cycle L1 latency that can issue 4 loads per cycle, you need 16 outstanding loads to hide L1 latency. Same logic at every level of the stack.

Memory-hierarchy latencies

Typical modern x86, in cycles:

Each level is ~3-10× slower than the previous. The "memory wall" is the gap from L1 to DRAM — 50-75× — and it's been growing for decades while compute has been getting cheaper. This single table dictates most cache-optimization strategy.

The bridge

Predict from a model, measure from a profiler, compare. The gap is where the optimization lives:

What you measure What model predicts Gap diagnosis
Observed GFLOPS Roofline ceiling at your kernel's operational intensity Below the roof = optimization headroom; on the roof = fundamentally limited
L1/L2/L3 miss rate (perf) Working set size vs cache capacity Working set just over cache = cache-thrashing; far over = stream-like
Branch mispredict rate (perf) ~0.1% for predictable branches; up to 50% for random data High rate on data-dependent branches → use branchless code or sort the data
Sustained memory bandwidth (STREAM) Theoretical channel bandwidth × number of channels × efficiency factor (~70–85%) Far below theory = NUMA misallocation, no prefetch, narrow access pattern
IPC (instructions per cycle) 4 for modern wide-issue x86; AVX-512 gives more FLOPs/cycle IPC near 1 with high cache hit rate = front-end bound or branch-dominated
Speedup curve vs cores Amdahl: ; USL with contention + coherency Curling early = small ; going retrograde = nonzero
Outstanding memory requests Little's law: Low concurrency on a memory-bound code = need software prefetch or more streams
TLB miss rate ~0 for arrays smaller than (4 KB × TLB entries); grows for sparse access High TLB miss = huge pages, sequential access, locality-improving layout
Lock contention time Critical-section time × thread count if all threads contend High = false sharing, oversized critical section, or wrong synchronization primitive
Power draw (joules) , integrated over time Energy / op = your real metric for energy efficiency (FLOPS/W, ops/J)

Which technique for which quantity

Match the technique to the bottleneck, never the reverse. The table below is organized by what you're bottlenecked ON, not by technique alphabetically. The most common mistake in performance engineering is applying a clever technique to the wrong bottleneck — vectorizing memory-bound code, threading already-bandwidth-saturated code, prefetching when you're compute-bound. Match first.

Technique Use when bottleneck is … Doesn't help when … Cost / complexity
SIMD vectorization (SSE/AVX/AVX-512, NEON, SVE) Compute-bound with data-parallel inner loops; high operational intensity Memory-bound; branch-heavy; non-data-parallel Compiler intrinsics or autovectorization; readability cost
Cache blocking / tiling Working set exceeds cache; data accessed multiple times Single-pass streaming; working set fits in cache already Algorithmic refactor; common in dense linear algebra (BLAS3)
AoS → SoA layout transformation Vectorized inner loop accesses one field per object Code accesses many fields together (cache locality favors AoS) Significant code rework; sometimes via wrapper structs
Software prefetching Predictable access pattern; latency-bound; hardware prefetcher missing Code with abundant memory-level parallelism already Compiler intrinsic; tricky to tune (distance + cache level)
Multi-threading (OpenMP, pthreads, TBB) Compute-bound code with parallelizable structure; Amdahl Single-socket bandwidth-bound; small problems (overhead) Synchronization correctness; race conditions
NUMA-aware allocation / first-touch policy Multi-socket bandwidth-bound; cross-socket traffic visible Single-socket systems; small problems Allocator API + initialization-thread discipline
Lock-free / wait-free data structures High lock-contention bottleneck; many threads on shared structure Low contention (mutex is fine); single-writer cases High complexity; memory-ordering subtlety; testing burden
Branchless / branch-friendly code Branch mispredict rate > ~5%; data-dependent branches Predictable branches (compiler already handles) Conditional moves (cmov), arithmetic predicates
GPU offload (CUDA, ROCm, OpenCL) Massively data-parallel kernels; large compute / data transfer ratio Frequent host-device transfer; control-flow-heavy code Different programming model; data-transfer overhead
Avoiding false sharing Multi-thread code with adjacent per-thread state in same cache line Single-thread or properly-padded data Padding / alignment annotations
Algorithmic complexity change Big-O is the bottleneck; problem grows quickly Constant-factor problems on small inputs Real algorithmic work; usually the highest payoff
JIT / specialization / constexpr Hot path depends on values known at runtime / compile time Code paths that vary unpredictably Compile-time programming or runtime code generation

One-sentence summary

Performance engineering is a measurement + modeling discipline, not a list of tricks. Profile your code (measurement), place it on the roofline and check Amdahl/USL (model), find the gap (bridge), pick a technique that matches the gap (methods table). The techniques in the methods table only help when applied to the bottleneck they target — match first, optimize second.

Related on this site