AnyLearn
All lessons
Computer Scienceintermediate

Why Most Code Is Memory Bound: The Roofline Model

Most real code never approaches a processor's arithmetic peak because it cannot be fed fast enough. The roofline model makes that concrete: plot operational intensity against achievable performance and the binding constraint becomes visible. This lesson covers the model, bandwidth versus latency, and the layout changes that follow.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 9

Peak performance is a number you will not reach

Vendor specifications quote peak floating-point throughput: so many operations per second, obtained by multiplying core count, clock, vector width, and operations per instruction. It is a real ceiling and almost no code gets near it.

The reason is not that processors are dishonest. It is that peak arithmetic assumes operands are already present. Nothing in that calculation accounts for getting data from DRAM, and the previous lesson established that a DRAM access is roughly sixty times an L1 hit.

So there are two ceilings, not one. A kernel is limited by arithmetic throughput or by the rate at which memory can supply operands, whichever binds first. The useful question is never "how fast is this processor" but "which of the two limits am I actually against", because the answer determines whether optimising arithmetic is worth any effort at all.

The roofline model answers that question on a single graph.

Full lesson text

All 9 steps on one page, for reading, reference, and search.

Show

1. Peak performance is a number you will not reach

Vendor specifications quote peak floating-point throughput: so many operations per second, obtained by multiplying core count, clock, vector width, and operations per instruction. It is a real ceiling and almost no code gets near it.

The reason is not that processors are dishonest. It is that peak arithmetic assumes operands are already present. Nothing in that calculation accounts for getting data from DRAM, and the previous lesson established that a DRAM access is roughly sixty times an L1 hit.

So there are two ceilings, not one. A kernel is limited by arithmetic throughput or by the rate at which memory can supply operands, whichever binds first. The useful question is never "how fast is this processor" but "which of the two limits am I actually against", because the answer determines whether optimising arithmetic is worth any effort at all.

The roofline model answers that question on a single graph.

2. Operational intensity

The bridge between the two ceilings is a single ratio.

operational intensity = operations performed / bytes moved

Measured in operations per byte, it says how much arithmetic a kernel extracts from each byte it drags out of memory. It is a property of the algorithm and its implementation, not of the hardware.

Williams, Waterman and Patterson introduced the model in "Roofline: An Insightful Visual Performance Model for Multicore Architectures", Communications of the ACM, volume 52, issue 4, April 2009. Their terminology is deliberate: they use operational intensity, traffic between caches and DRAM, rather than the older arithmetic intensity, which measures traffic between the processor and cache. The distinction matters, because for anything larger than cache it is the DRAM traffic that binds.

Low intensity means the kernel is memory hungry. High intensity means it does substantial work per byte and has a chance of reaching the arithmetic ceiling.

3. Computing intensity for real kernels

Work three standard cases, assuming 4-byte floats and that nothing is reused from cache.

Vector addition, c[i] = a[i] + b[i]:

per element: 1 add, 12 bytes moved (read a, read b, write c)
intensity  = 1 / 12 = 0.083 ops/byte

Dot product, s += a[i] * b[i]:

per element: 2 ops (multiply, add), 8 bytes read
intensity  = 2 / 8 = 0.25 ops/byte

Dense matrix multiply, N-by-N, with good blocking:

total ops   = 2 * N^3
total bytes = 3 * N^2 * 4   (each matrix read or written once)
intensity   = 2N^3 / 12N^2 = N / 6 ops/byte

That last result is the important one. Matrix multiply's intensity grows with N, because each element loaded participates in N multiplications. Vector addition's intensity is a constant, and a small one. This is exactly why dense linear algebra can approach peak arithmetic while vector operations never do, and why blocking mattered so much in the previous lesson: without it, matrix multiply does not achieve the reuse this calculation assumes.

4. The roofline

Plotted on log-log axes with intensity on the x-axis and attainable performance on the y-axis, the ceiling is the minimum of two lines. A diagonal, where performance equals intensity multiplied by peak memory bandwidth, and a horizontal line at peak compute. Their intersection is the ridge point. A kernel to the left of the ridge is memory bound and no amount of arithmetic optimisation will help it. A kernel to the right is compute bound. The ridge point is a property of the machine and is a compact statement of how badly balanced it is.

flowchart LR
  A["Operational intensity, ops per byte"] --> B["Low intensity region"]
  A --> C["High intensity region"]
  B --> D["Ceiling = intensity x peak bandwidth"]
  C --> E["Ceiling = peak compute"]
  D --> F["Memory bound: sloped roof"]
  E --> G["Compute bound: flat roof"]
  F --> H["Ridge point where they meet"]
  G --> H

5. Reading the model, with a worked case

Take a machine with 200 GFLOP/s peak and 50 GB/s of memory bandwidth.

ridge point = peak compute / peak bandwidth
            = 200e9 / 50e9
            = 4 ops/byte

Any kernel below 4 operations per byte is memory bound on this machine. Now revisit the three kernels:

vector add    0.083 ops/byte -> attainable = 0.083 x 50e9 =  4.2 GFLOP/s   (2% of peak)
dot product   0.25  ops/byte -> attainable = 0.25  x 50e9 = 12.5 GFLOP/s   (6% of peak)
matmul N=1024 171   ops/byte -> compute bound, 200 GFLOP/s available

Vector addition can reach about 2% of this machine's arithmetic peak, and that is not a defect in the code. It is the ceiling. Hand-optimising its inner loop, vectorising it further, or unrolling it cannot move the number, because the constraint is bandwidth.

This is the model's real value. It tells you which optimisations are pointless before you spend a week on them.

6. Bandwidth and latency are different problems

The roofline models bandwidth, and it is worth being precise that this is not the same constraint as latency, which the previous lesson measured.

  • Latency is how long one access takes. Roughly 246 cycles to DRAM on the Skylake part measured earlier.
  • Bandwidth is how many bytes per second arrive when many accesses are in flight at once.

They relate through Little's Law: the concurrency needed to sustain a given bandwidth equals bandwidth multiplied by latency. To keep 50 GB/s flowing with an 80 ns latency you need roughly 4000 bytes, about 62 cache lines, in flight continuously.

That is why the structures from lesson two matter here. A core with 192 load queue entries can track that many outstanding accesses; a core with 8 could not, and would be latency bound long before reaching its bandwidth ceiling.

The practical consequence: a program can fail to reach peak bandwidth simply by not having enough independent accesses outstanding. Pointer chasing is the extreme, with exactly one access in flight at a time, so it achieves a tiny fraction of bandwidth regardless of how much the hardware could deliver.

7. Prefetching, and the access patterns it can follow

Processors try to hide latency by fetching lines before they are requested. Hardware prefetchers detect regular patterns and issue speculative loads ahead of the access stream.

What they handle well is narrow, and worth knowing precisely:

  • Sequential forward and backward traversal.
  • Constant-stride access, up to a limit, within a page boundary.

What defeats them:

  • Indirect access, a[b[i]], where the address depends on loaded data.
  • Pointer chasing through a linked structure.
  • Random access.
  • Strides that cross page boundaries, since a hardware prefetcher generally will not prefetch across pages without a translation for the next one.

This is the mechanical reason an array of structs traversed linearly outruns a linked list holding identical data. The complexity is the same and the operation count is the same, but one pattern the prefetcher can follow and the other it cannot.

8. Layout: array of structs versus struct of arrays

Once you accept that intensity and access pattern govern performance, data layout becomes a first-order decision rather than a style preference.

// Array of Structs: fields interleaved per particle
struct Particle { float x, y, z, vx, vy, vz, mass, charge; };
struct Particle particles[N];        // 32 bytes each

// Struct of Arrays: each field contiguous
struct Particles {
    float x[N], y[N], z[N];
    float vx[N], vy[N], vz[N];
    float mass[N], charge[N];
};

Now suppose a loop reads only x. With the array of structs, each 64-byte line holds two whole particles, so it carries 8 bytes of wanted x and 56 bytes of fields the loop never touches. You use one eighth of the bandwidth you paid for. With the struct of arrays, a line holds 16 consecutive x values and every byte is used.

Neither layout is correct in general. If the loop touches every field of one particle at a time, the array of structs wins for exactly the same reason reversed. The rule is to lay data out along the direction it is traversed, which requires knowing the access pattern before choosing the type.

9. A checklist that follows from the model

Putting the cursus together, here is the order in which to attack a slow kernel.

  1. Measure intensity first. Count operations and bytes moved. If you are far below the ridge point, the kernel is memory bound and arithmetic optimisation is wasted effort. This step costs minutes and routinely saves days.
  2. Raise intensity if you can. Fusing two passes over an array into one halves the traffic and doubles intensity. Blocking creates reuse that was not there before. These move the kernel rightward on the graph, which is the only way a memory-bound kernel gets faster.
  3. Reduce bytes moved. Smaller types, better layout, dropping unused fields from the hot structure. A float32 instead of float64 halves the traffic exactly.
  4. Improve the access pattern. Make it something a prefetcher can follow, and make sure enough independent accesses are outstanding to fill the latency-bandwidth product.
  5. Only then optimise arithmetic. Vectorisation, instruction selection and unrolling matter once, and only once, you are actually against the flat part of the roof.

The general lesson of all four lessons is one sentence: on a modern processor, arithmetic is nearly free and data movement is what you pay for.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. A machine has 200 GFLOP/s peak compute and 50 GB/s bandwidth. Where is its ridge point?
    • 0.25 ops/byte
    • 4 ops/byte
    • 40 ops/byte
    • 250 ops/byte
  2. Why does dense matrix multiply reach a much higher fraction of peak than vector addition?
    • It uses fewer branches, so it suffers fewer mispredictions
    • Its operations vectorise better than element-wise addition
    • Its operational intensity grows with N, since each loaded element is reused N times
    • It has a smaller working set, so it stays in cache
  3. Why do the terms 'operational intensity' and 'arithmetic intensity' differ in the roofline paper?
    • Operational intensity counts memory operations, arithmetic intensity counts floating-point ones
    • They are synonyms and the paper uses them interchangeably
    • Operational intensity measures cache-to-DRAM traffic, arithmetic intensity processor-to-cache
    • Arithmetic intensity applies only to single-core machines
  4. According to Little's Law, what is needed to sustain 50 GB/s at 80 ns latency?
    • About 4000 bytes, roughly 62 cache lines, in flight at once
    • About 400 bytes, roughly 6 cache lines, in flight at once
    • A single outstanding access, since bandwidth is independent of latency
    • About 40 KB, roughly 625 cache lines, in flight at once
  5. A loop reads only the `x` field of every particle. Which layout uses bandwidth efficiently, and why?
    • Array of structs, because each particle stays contiguous
    • Struct of arrays, because a cache line then holds only `x` values
    • Both perform identically, since the same number of bytes is requested
    • Array of structs, because it aligns better to 64-byte boundaries

Related lessons

AI
advanced

Finding the Next One: Fusion Beyond Attention

The pattern that made attention slow recurs across the stack, and once you know what to look for it is easy to find. This lesson applies the diagnosis to normalisation layers, optimizer steps, loss functions and inference decoding, covers why fused attention silently stops applying when a model deviates slightly from standard, and gives the profiling routine that decides where to look first.

10 steps·~15 min
AI
advanced

Writing Fused Kernels Without Writing CUDA

The reason most teams never fuse anything is that CUDA asks you to manage threads, shared memory and synchronisation by hand. Triton moves the unit of programming from a thread to a block and hands the rest to a compiler. This lesson covers what that buys, what it still asks of you, how to decide a kernel is worth writing, and how to be sure it is correct.

10 steps·~15 min
AI
advanced

Attention Is Memory-Bound, and Nobody Noticed for Five Years

For years attention was optimised by reducing FLOPs, and approximate methods that cut FLOPs kept failing to run faster. The reason is that attention was never compute-bound: it spends its time moving a matrix between GPU memory tiers. This lesson establishes that hierarchy, counts the traffic a standard implementation generates, and shows why an exact algorithm beat every approximation.

10 steps·~15 min
Programming
advanced

Scheduling and Resources: Requests Are Not Limits

Two numbers govern where a pod lands and how it behaves under pressure, and they do completely different jobs. Requests are used for placement and are a promise; limits are enforced at run time and are a ceiling. This lesson separates them, covers the asymmetry between CPU and memory enforcement, and explains what actually happens when a node runs out.

8 steps·~12 min