SIMDSIMDvectorlanesdata parallelismthroughput

SIMD: One Instruction, Many Elements

A vector register holds several values and a vector instruction applies one operation to all of them at once. It is the cheapest parallelism on the machine — single-threaded, race-free, and frequently left unused because a single unprovable pointer relationship disabled it.

▶ Run the labFollow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
How does one instruction operate on eight numbers at once, and what does my data have to look like for that to be possible?
What you wrote
A loop processes one element per iteration. Eight elements means eight iterations, each with its own add.
What the hardware does
A vector register holds several elements side by side in independent lanes. A single vector instruction issues once and the execution unit applies the operation to every lane simultaneously, producing several results in one operation slot.
For uniform arithmetic over contiguous data, it is a multiple-times speedup available without threads, without synchronisation and without any possibility of a race. It is also the mechanism most often silently lost to a layout or aliasing problem the programmer never sees.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

Lanes

A vector register of a given width, divided by the element size, gives the number of lanes. The same physical register holds a different number of elements depending on the type: twice as many 32-bit floats as 64-bit doubles, four times as many 8-bit integers as 32-bit ones. Narrower types therefore vectorise better, which is one of the real performance arguments for using the smallest type that is correct.

Each lane is independent. Lane 3 computing a[3] + b[3] cannot observe or affect lane 0, and there is no communication between them in a basic arithmetic operation. That independence is exactly why the requirement on your code is that the iterations be independent — the hardware has no mechanism to carry a value from one lane to the next.

Operations that need to cross lanes — a horizontal sum, a shuffle, a reduction — exist but are a different and generally more expensive class. This is why reductions vectorise into per-lane partial results followed by one horizontal combine at the end, exactly mirroring the multiple-accumulator technique from Dependency Graphs: The Real Shape of Your Code.

One 256-bit register viewed as different element types. Widths are ISA-SPECIFIC; the principle is not.
256 bits as 32-bit floats  -> 8 lanes
  [ a0 | a1 | a2 | a3 | a4 | a5 | a6 | a7 ]
+ [ b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 ]
= [ c0 | c1 | c2 | c3 | c4 | c5 | c6 | c7 ]      one instruction

256 bits as 64-bit doubles -> 4 lanes
  [   a0    |   a1    |   a2    |   a3    ]

256 bits as 8-bit integers -> 32 lanes
  [a0|a1|a2|a3| ... 32 elements ... |a31]

Narrower element type, more lanes, more work per instruction.

What has to be true of your loop

Three conditions, and all three must hold. Independence: iteration i must not depend on iteration i-1, because lanes execute together and cannot be ordered relative to one another. Contiguity: the elements should be adjacent in memory, so one wide load fills a register — gathering scattered elements is supported on some instruction sets but costs far more. Uniformity: every lane does the same operation, so data-dependent branching within the loop body forces the hardware to execute both paths and discard the unwanted lanes.

The loop below satisfies all three and is the canonical vectorisable shape: read contiguous, compute uniformly, write contiguous, no cross-iteration dependency. The scalar version does the same arithmetic one element at a time; the vector version does it several elements per instruction, with the same total number of additions performed by fewer instructions.

Notice the trip-count condition hiding at the end. Vector loops need a scalar remainder for elements that do not fill a final register, and the setup has a fixed cost. For very short loops that overhead can exceed the benefit, which is one legitimate reason a compiler declines to vectorise something that looks eligible.

The same arithmetic, scalar and vector. SIMPLIFIED — real intrinsics are ISA-specific.
1// scalar: one element per iteration
2for i in 0..n:
3 c[i] = a[i] + b[i]
4
5// vector: LANES elements per iteration
6i = 0
7while i + LANES <= n:
8 va = vector_load(a, i) // one wide load
9 vb = vector_load(b, i)
10 vc = vector_add(va, vb) // one instruction, LANES adds
11 vector_store(c, i, vc)
12 i = i + LANES
13
14// scalar remainder for the leftover elements
15while i < n:
16 c[i] = a[i] + b[i]
17 i = i + 1

Why it is not always a win

ISA-SPECIFICVector width, available element types, gather and scatter support, and masked-execution facilities differ substantially between instruction sets and between extension levels within one ISA. Fixed-width and scalable-vector designs also differ in how the remainder loop is handled — sometimes eliminating it entirely.

Vectorisation increases the rate at which a core consumes memory. A loop doing one add per element loaded is already close to memory-bound in scalar form on many machines; making the arithmetic eight times faster does not help when the limit was fetching the data. The metric that predicts this is arithmetic intensity — operations performed per byte moved — and low-intensity kernels see little benefit (When the Memory Bus Is the Bottleneck).

There is also a frequency consideration on some designs. Sustained heavy vector work can draw enough power that the core reduces its clock, so the vector speedup is partially offset by every instruction running at a lower frequency, including the scalar code around it. This is strongly MICROARCH-SPECIFIC — it is pronounced on some server parts with wide vector units and negligible elsewhere — and it is a real reason to measure rather than assume (The Clock Is a Variable, Performance Per Watt).

And branches inside the loop body undo the uniformity requirement. When lanes need to take different paths, the usual implementation executes both sides and merges with a mask, so the loop pays for all paths regardless of the data. A branchy loop can be slower vectorised than scalar, which connects directly to Branchless Code: A Trade, Not an Upgrade and to why predictability matters even where there is no branch predictor involved.

  • Helps most — high arithmetic intensity, contiguous data, no branching, long trip count.
  • Helps little — memory-bound loops where bandwidth already binds.
  • Can hurt — heavy branching per element, very short loops, or sustained wide-vector work that lowers clock.
  • Cannot apply — loop-carried dependencies, or addresses only known one at a time (Pointer Chasing: The Address You Do Not Have Yet).

Key points

  • A vector register holds several elements in independent lanes; one instruction operates on all of them.
  • Lane count depends on element size, so narrower types vectorise better.
  • Three conditions must hold: independent iterations, contiguous data, uniform operations.
  • Cross-lane operations exist but cost more, which is why reductions become per-lane partials plus one final combine.
  • It increases memory demand, so a bandwidth-bound loop gains little and a branchy loop can lose.

SIMD Lanes

Change an input and watch which number moves — and which one refuses to.

c[i] = a[i] + b[i] over 16 elements
instr 1
instr 2
instr 3
instr 4
instructions
4
elements each
4
vs scalar
4× fewer

Fewer instructions for identical work — but only when the elements are independent, contiguous and numerous enough to amortise the setup. A loop with a carried dependency, a data-dependent branch or a scattered access pattern will not vectorize no matter how wide the hardware is.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Wide load → vector register: one memory operation fills all lanes from contiguous addresses.
  2. 2
    Vector register → vector unit: one instruction issues to a vector execution port, occupying one slot.
  3. 3
    Vector unit → lanes: the operation is applied to every lane simultaneously and independently.
  4. 4
    Lanes → wide store: results are written back to contiguous memory in one operation.
  5. 5
    Remainder → scalar loop: elements that do not fill a full register are handled one at a time afterwards.
What people conclude from this — wrongly
  • "Vectorising always speeds up a loop." Not if the loop is already bandwidth-bound.
  • "SIMD is a form of multithreading." It is single-threaded and cannot race.
  • "IPC dropped, so vectorising made it worse." Fewer instructions doing the same work lowers IPC by design.
  • "The compiler will vectorise anything shaped like a loop." Aliasing alone stops it routinely.
  • "Wider vectors are always better." Wider registers can reduce clock on some designs, and only help if bandwidth allows.

Consequences, controls and cost

What it causes
  • • Instruction count falls by roughly the lane count while total arithmetic stays the same, so IPC often falls while time falls further ([[ipc]]).
  • • Memory bandwidth demand rises, which can convert a compute-bound loop into a bandwidth-bound one.
  • • Data layout becomes performance-critical, since non-contiguous elements cannot be loaded in one operation.
  • • Loops with data-dependent branches execute all paths under masks and lose much of the benefit.
  • • Very short loops may run slower vectorised because of setup and remainder overhead.
What you can do
  • • Lay data out contiguously by the field you process, which is the core of [[aos-vs-soa]].
  • • Use the narrowest correct element type to increase lane count.
  • • Remove data-dependent branching from the loop body, or restructure into separate uniform passes.
  • • Break loop-carried dependencies so iterations are genuinely independent.
  • • Verify the compiler actually vectorised it rather than assuming ([[auto-vectorization]]).
How to see it
  • • Inspect the generated code for vector instructions in the hot loop — the definitive check that it happened at all.
  • • Compare elapsed time against the scalar version on the same data and machine; do not use IPC as the criterion.
  • • Estimate arithmetic intensity — operations per byte loaded — to predict whether bandwidth will cap the benefit.
  • • Watch achieved clock during sustained vector work on server parts where downclocking is documented.
  • • Test with realistic trip counts, since short loops behave very differently from long ones.
What it costs
  • • Explicit vector code is ISA-specific and needs a scalar fallback path, doubling the code to maintain.
  • • Layouts chosen for vectorisation ([[aos-vs-soa]]) can worsen locality for code that touches whole records.
  • • Vector code is harder to read and to debug, and lane-level bugs are unpleasant to diagnose.
  • • Sustained wide-vector work can reduce clock for surrounding scalar code on some designs.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • ISA-SPECIFICVector width, element types, masking and gather support differ by instruction set and by extension level. Code written against one extension will not run on a machine without it, so a runtime check or multiple builds are usually required.
  • MICROARCH-SPECIFICThroughput per vector port, and whether sustained wide-vector work reduces clock, are properties of a specific core. The downclocking effect is pronounced on some server designs and absent on others.

Misconceptions

Claim
“SIMD means the CPU runs my loop on multiple cores.”
Reality
It is entirely within one core and one thread. No scheduler is involved, no synchronisation is needed and no race is possible — which is exactly what makes it cheap to adopt.
Claim
“If my loop is simple arithmetic, it will be vectorised.”
Reality
Simplicity is not sufficient. The compiler must also prove the arrays do not overlap, that iterations are independent and that the trip count justifies the setup. Any one of those failing silently disables it.
Claim
“Vectorisation is limited by how many arithmetic units the core has.”
Reality
For most real loops it is limited by memory. Vector units are usually capable of consuming data faster than the memory system can supply it, which is why arithmetic intensity predicts the benefit better than vector width.

Apply it