Loop Transformations and the Polyhedral Model

Programs spend most of their time in loops, so loops are where an optimising compiler earns its keep. Hoisting invariant code out of a loop was a first taste; the transforms on this page are far more dramatic. They reshape the loop nest itself — swapping which loop is inner, chopping the iteration space into cache-sized blocks, welding two loops into one — to win locality and parallelism, all while computing exactly the same values. The governing question, always, is: which reshapings preserve the program's meaning? The answer is the theory of data dependence, and its most powerful modern expression is the polyhedral model.

The catalogue of loop-nest transforms

A handful of transformations recur throughout every optimising compiler. Each targets a specific enemy — poor cache behaviour, loop overhead, or hidden parallelism:

TransformWhat it doesChiefly buys
Interchangeswaps the order of two loops in a neststride-1 access → cache locality; exposes parallelism
Tiling (blocking)splits a loop into blocks that fit in cache, iterating block-by-blocktemporal locality — reuse data before it is evicted
Fusionmerges two adjacent loops into one bodyreuse across loops; less loop overhead
Fission (distribution)splits one loop into twoseparates a parallel part from a serial one; helps vectorization
Unrollingreplicates the body u times, stepping by uless branch/counter overhead; more ILP to schedule
Skewingreshapes the iteration space (adds one index to another) to make dependences march diagonallyexposes a parallel wavefront in otherwise-serial nests

Tiling deserves a second look because it is the great locality win. Matrix multiply over large N streams a whole row and column per output element; by the time you revisit a cache line it has long been evicted. Tiling restructures the triple loop to work on b\times b sub-blocks that fit in cache, turning O(N^3) cache misses into a small fraction of that — often a several-fold speedup with zero change to the arithmetic.

Data dependence: the law every transform must obey

Two statement instances are dependent if they access the same memory location and at least one writes. Three flavours matter:

For a loop, we summarise a dependence by a distance vector \vec{d} = \vec{i}_{\text{sink}} - \vec{i}_{\text{source}}: how many iterations apart (per loop level) the two accessing iterations are. In A[i] = A[i-1] + 1, iteration i reads what iteration i-1 wrote, so the distance is d = 1 — a loop-carried dependence. When only the sign matters we write a direction vector with entries (<, =, >). The golden rule:

Seeing the iteration space

The clearest way to picture all this is to plot the iteration space: one point per loop iteration, arranged on the integer grid, with an arrow drawn for each dependence. Below is the classic stencil A[i][j] = A[i-1][j] + A[i][j-1]: each point depends on its west and south neighbours, giving distance vectors (1,0) and (0,1).

Both distance vectors are lexicographically positive, so the loops could legally be interchanged; but notice that no arrow is purely along one axis in the skewed sense — points on an anti-diagonal i+j = \text{const} have no arrows between them, so an entire anti-diagonal can execute in parallel as a wavefront. Reading legality and parallelism straight off the arrows is the whole art.

The polyhedral model: geometry as the unifying framework

The ad-hoc catalogue above hides a deep unity. In the polyhedral model, a loop nest with affine bounds and affine array subscripts is described exactly by three mathematical objects:

The magic: every classical transform — interchange, tiling, fusion, skewing, and arbitrary compositions of them — is just a different choice of affine schedule applied to the same domain. Legality becomes a single question — does the new schedule keep every dependence lexicographically positive? — that reduces to integer linear feasibility. Optimisers such as Pluto and LLVM/GCC's Polly/Graphite search the space of legal affine schedules for one that maximises locality and parallelism, unifying the entire catalogue into one optimisation problem.

\text{transform} \;=\; \text{new affine schedule } \theta \quad\text{legal} \iff \theta(\vec{i}_{\text{sink}}) \succ \theta(\vec{i}_{\text{source}}) \text{ for every dependence.}

Testing legality of interchange, in code

Given the distance vectors of a 2-deep loop nest, loop interchange (swapping the two loops) is legal exactly when swapping the two components of every distance vector keeps it lexicographically positive. The one thing interchange cannot survive is a distance vector like (1,-1): swap it to (-1,1) and it becomes lexicographically negative — the sink would run before its source.

type Vec = [number, number]; // Lexicographically positive: first non-zero component is > 0. function lexPositive(v: Vec): boolean { for (const c of v) { if (c > 0) return true; if (c < 0) return false; } return false; // all zero: not a real cross-iteration dependence } // Interchange swaps the two loop levels, i.e. swaps the two components of every distance vector. function interchangeLegal(deps: Vec[]): boolean { return deps.every((d) => lexPositive([d[1], d[0]])); } // Stencil A[i][j] = A[i-1][j] + A[i][j-1]: distances (1,0) and (0,1). const stencil: Vec[] = [[1, 0], [0, 1]]; console.log("stencil interchange legal? " + interchangeLegal(stencil)); // true // A skewed dependence (1,-1) blocks interchange. const skewed: Vec[] = [[1, -1]]; console.log("(1,-1) interchange legal? " + interchangeLegal(skewed)); // false console.log(" because swapped (-1,1) lex-positive? " + lexPositive([-1, 1]));

The stencil interchanges freely; the skewed nest does not. That single check — "is the reordered distance vector still lexicographically positive?" — is, at heart, the entire legality test the polyhedral model generalises to arbitrary affine schedules.

Because the iteration domain of an affine loop nest is literally a polyhedron — an intersection of half-spaces, each half-space being one loop bound a\cdot\vec{i} \le b. A doubly-nested triangular loop \{0\le i<N,\ 0\le j\le i\} is a triangle; a rectangular nest is a box; tiling literally slices the polytope into smaller polytopes. Because the whole loop nest is captured as geometry, transformations are affine maps of that geometry, and the machinery of integer linear programming (via tools like isl, the integer set library) can reason about them exactly rather than pattern-matching source syntax. Turning a compiler-optimisation question into a computational-geometry question is what makes the model so powerful — and why it can discover transform compositions a human would never hand-code.

The seductive error is to justify a transform by the benefit — "tiling improves locality, so tile" — without checking dependences. Tiling, fusion and interchange are only legal when they respect every dependence; a single loop-carried dependence pointing the wrong way after the transform makes it illegal, and the compiler will silently compute wrong answers. Tiling a loop whose iterations must run strictly in order (a recurrence like x[i] = x[i-1] * a) does not magically become parallel just because the blocks fit in cache — the dependence (1) crosses every tile boundary. Locality is the reward; dependence legality is the gate, and the gate comes first. Skewing exists precisely to make an otherwise-illegal tiling legal by reorienting the dependences first.