A scalar loop adds one pair of numbers per iteration. A SIMD instruction adds eight pairs at once. The compiler will make that swap for you automatically — if the loop is shaped in a way it can prove is safe. Most “slow” numeric loops are just loops the optimizer refused to vectorize, and the reasons are knowable.
What SIMD Actually Is
SIMD — Single Instruction, Multiple Data — packs several values into one wide register and operates on all of them with one instruction:
scalar: a0+b0, a1+b1, a2+b2, a3+b3 (4 instructions)
SIMD: [a0 a1 a2 a3] + [b0 b1 b2 b3]
= [a0+b0 a1+b1 a2+b2 a3+b3] (1 instruction)A 256-bit AVX register holds eight 32-bit floats. Done right, that’s an 8× arithmetic speedup before you’ve touched threads.
The Loop the Compiler Loves
void add(float *a, const float *b, const float *c, int n) {
for (int i = 0; i < n; i++)
a[i] = b[i] + c[i];
}This vectorizes cleanly: each iteration is independent, the access pattern is contiguous, and there are no branches. The compiler emits a vectorized body that handles 8 elements per step plus a scalar “remainder” loop for the leftover.
What Quietly Blocks It
Pointer aliasing. The compiler can’t prove a doesn’t overlap b or c. If
it does, processing 8 at once would read values that an earlier store changed.
So it bails out — unless you promise they don’t overlap:
void add(float *restrict a, const float *restrict b,
const float *restrict c, int n) { ... }restrict is often the single keyword that unlocks vectorization.
Loop-carried dependencies. If iteration i needs the result of i-1, the
operations can’t run in parallel:
for (int i = 1; i < n; i++)
a[i] = a[i-1] * 2; // each step depends on the last — not vectorizableBranches and calls inside the loop. A non-inlinable function call or a data-dependent branch usually stops vectorization, though compilers can sometimes turn simple branches into masked SIMD operations.
See What It Did
Don’t guess — ask the compiler:
# GCC: report which loops vectorized and why others didn't
gcc -O3 -march=native -fopt-info-vec-missed add.c
# Clang: same idea
clang -O3 -march=native -Rpass-analysis=loop-vectorize add.cNote -march=native: without it, the compiler targets a baseline CPU and may
avoid the widest instructions your machine actually has.
Takeaways
- SIMD does N lanes of arithmetic in one instruction; compilers auto-vectorize
eligible loops at
-O3. - The usual blockers are pointer aliasing, loop-carried dependencies, and branches/calls in the loop body.
restrictand-march=nativeunlock most missed cases — and the optimizer’s vectorization report tells you exactly what stopped it.