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.
Software view, hardware view
The gap between what you wrote and what the machine does is where this whole domain lives.
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.
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.
1// scalar: one element per iteration2for i in 0..n:3 c[i] = a[i] + b[i]4 5// vector: LANES elements per iteration6i = 07while i + LANES <= n:8 va = vector_load(a, i) // one wide load9 vb = vector_load(b, i)10 vc = vector_add(va, vb) // one instruction, LANES adds11 vector_store(c, i, vc)12 i = i + LANES13 14// scalar remainder for the leftover elements15while i < n:16 c[i] = a[i] + b[i]17 i = i + 1Why it is not always a win
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.
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.
- 1Wide load → vector register: one memory operation fills all lanes from contiguous addresses.
- 2Vector register → vector unit: one instruction issues to a vector execution port, occupying one slot.
- 3Vector unit → lanes: the operation is applied to every lane simultaneously and independently.
- 4Lanes → wide store: results are written back to contiguous memory in one operation.
- 5Remainder → scalar loop: elements that do not fill a full register are handled one at a time afterwards.
- • "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
- • 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.
- • 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]]).
- • 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.
- • 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.
- 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.