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:

text
   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

c
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:

c
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:

c
for (int i = 1; i < n; i++)
    a[i] = a[i-1] * 2;   // each step depends on the last — not vectorizable

Branches 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:

bash
# 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.c

Note -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.
  • restrict and -march=native unlock most missed cases — and the optimizer’s vectorization report tells you exactly what stopped it.