A modern CPU core is secretly a small parallel machine. Its SIMD units (SSE, AVX, NEON, SVE) apply one instruction to a whole vector of 4, 8, or 16 elements at once; multiple cores can run independent iterations simultaneously. But most source code is written as plain scalar loops — one element at a time. The job of a parallelizing and vectorizing compiler is to discover the parallelism latent in those loops and map it onto the hardware's lanes and cores, without the programmer rewriting anything. It is one of the highest-leverage optimisations there is: a single loop, correctly vectorized, can run several times faster on the exact same chip.
This work stands squarely on the theory of
Compilers exploit two distinct granularities, and it helps to keep them separate:
A loop-carried dependence is one that flows from one iteration to a later one.
The loop a[i] = b[i] + c[i] has none — iteration 5 neither needs nor disturbs iteration 4,
so it is DOALL and vectorizes trivially: load four bs, four cs, add all four in
one instruction, store four as. By contrast a[i] = a[i-1] + b[i] carries a
dependence of distance 1 — each iteration needs the previous iteration's result — so its lanes
cannot run at once. The picture below contrasts the two:
On the left, four independent iterations drop cleanly into four SIMD lanes and execute in one vector step. On the right, the arrows chain iteration to iteration — a serial recurrence that no amount of wishful thinking will let you run in parallel as written.
Some loops look serial but aren't quite. A reduction —
sum = sum + a[i] — has a genuine loop-carried dependence on sum, yet it can
still be parallelized, because the combining operation (+) is associative.
The compiler recognises the reduction pattern and splits it: give each lane (or thread) its own partial
accumulator, run the independent partial sums in parallel, then combine the partials at the end.
There is a subtlety with floating point: real addition is associative, but floating-point
addition is not (rounding makes
To vectorize A[2*i + 1] = A[3*i] + 1 the compiler must decide whether a write in one
iteration can ever hit the same array element as a read in another. Solving that exactly is integer
linear programming; in practice compilers run a ladder of cheap, conservative dependence
tests, from fastest-and-weakest to slowest-and-strongest:
Note the logic: these tests prove the absence of a dependence. Failing the GCD condition is a certificate of independence. Passing it (gcd divides the difference) does not prove a dependence exists — only that one might, so the compiler must stay conservative and not parallelize (or run a stronger test).
Here is the GCD test deciding whether a dependence can exist between two affine array subscripts in a loop. If the gcd of the coefficients does not divide the constant gap, independence is proven and the loop is safe to vectorize.
The first and third loops are certified independent by a single divisibility check; the classic
A[i] = A[i-1] recurrence passes the test (gcd 1 divides everything), so the compiler cannot
rule out the dependence — exactly the loop that must stay serial.
If dependence testing were the whole story, compilers would vectorize everything. Reality fights back on several fronts:
A[index[i]] gathers)
defeat static dependence tests entirely.
Because of all this, languages give programmers ways to promise what the compiler cannot prove.
C's restrict asserts non-aliasing; #pragma omp simd and
#pragma omp parallel for assert that a loop is safe to vectorize or parallelize; alignment
attributes assert address alignment. These hints don't make the compiler smarter — they shift the
proof obligation onto the programmer, letting the compiler proceed where its own analysis had to
give up.
Superword-Level Parallelism (SLP) vectorizes straight-line code, no loop
required. Imagine transforming an RGBA pixel: r2 = r*s; g2 = g*s; b2 = b*s; a2 = a*s — four
independent scalar multiplies by the same factor. An SLP pass spots that these isomorphic operations can
be bundled into a single 4-wide vector multiply, packing the four scalars into one vector register. It is
the perfect complement to loop vectorization: loop vectorization finds parallelism across
iterations, SLP finds it within a basic block among sibling operations. Modern compilers run
both, and much hand-written "SIMD intrinsics" code is really just doing by hand what an SLP vectorizer
does automatically.
Vectorization is all-or-nothing per loop: pack four iterations into one vector step and they execute
simultaneously, so if any of those four needs a value another of the four is still
computing, the result is wrong. A single true loop-carried dependence — one A[i] = A[i-1] + …
anywhere in the body — is enough to block the entire loop, even if every other statement is embarrassingly
parallel (the cure is often loop fission to split the serial statement out). Two more traps
follow close behind. Reductions look like blocking dependences but are legal to
parallelize only when the operator is associative — and for floating point that means accepting a
different rounding, so it needs an explicit fast-math opt-in. And alignment/remainder:
the vector body handles iterations in chunks of the vector width, so a scalar remainder loop must mop up
the leftover