Part 5 · SIMD and the modern ISA
Vector registers
A second, wider register file with its own instructions. The register name tells you the width, and the width tells you the parallelism.
What this lesson is trying to break (2)
- SIMD is an exotic feature you have to opt into.
- Wider vectors are always faster.
You have already seen these registers — every vectorised loop in Part 3 used them. This part names them.
A second register file
Floating point and SIMD live in a separate bank from the general-purpose
registers, with their own instructions. On x86-64 the same registers serve both
scalar floats and vectors, which is why xmm0 appears in code that does no SIMD at
all.
Varies: only the register width
| Name | Bits | ISA | int32 lanes | double lanes |
|---|---|---|---|---|
xmm0–xmm15 | 128 | SSE, SSE2… | 4 | 2 |
ymm0–ymm15 | 256 | AVX, AVX2 | 8 | 4 |
zmm0–zmm31 | 512 | AVX-512 | 16 | 8 |
v0–v31 (ARM64) | 128 | NEON | 4 | 2 |
The name is the width. Seeing ymm in a listing tells you 8-lane int32 parallelism without reading anything else. Note that your M1's NEON is 128-bit, so ARM64 code here is 4 lanes where AVX2 gets 8 — a real difference when comparing benchmarks across machines.
Like rax/eax, these overlap: xmm0 is the low half of ymm0, which is the
low quarter of zmm0.
Reading the mnemonics
Vector instruction names look like noise until you know they are systematic.
Floating point — the suffix is packing plus precision:
| Suffix | Meaning |
|---|---|
ss |
scalar single — one float |
sd |
scalar double — one double |
ps |
packed single — 4/8/16 floats |
pd |
packed double — 2/4/8 doubles |
So addss adds one float, addps adds four or eight. Scalar float code is
therefore full of xmm registers doing no parallel work at all — which surprises
people who assume xmm means SIMD.
Integer — a p prefix for “packed”, and a final letter for element width:
paddb bytes · paddw 16-bit words · paddd 32-bit dwords · paddq 64-bit
qwords
And a v prefix means the AVX encoding, which is three-operand:
vpaddd ymm0, ymm1, ymm2 is ymm0 = ymm1 + ymm2, non-destructive. The older
paddd xmm0, xmm1 is xmm0 += xmm1.
vpaddd ymm0, ymm1, ymm2 · paddd xmm0, xmm1 · add v0.4s, v1.4s, v2.4s
Vectorised integer addition. Read the width off the register name and the element size off the suffix.
The ARM64 form is the most self-describing of the three: v1.4s means “four
32-bit lanes”, stated in the operand.
When you see any of these in a loop, the compiler autovectorised — so the loop was free of the obstacles in 3.4.
The horizontal problem
SIMD is good at doing the same thing to many lanes, and bad at combining lanes.
Adding two vectors is one instruction. Adding the four lanes within one vector —
a horizontal operation — takes a cascade: shift the vector, add, shift again,
add. You saw this in 0.2 as psrldq/paddd pairs at
a loop’s exit.
That asymmetry shapes how vectorised code is written: keep partial results in lanes for as long as possible and combine once at the end. A loop that horizontally sums every iteration gets no benefit at all. It is also why a reduction (summing an array) is harder to vectorise than a map (scaling an array), and why the tail of a vectorised reduction always looks messy.
Wider is not automatically better
Three reasons, all real:
Not enough data. Sixteen lanes need at least sixteen elements to be worth the prologue and epilogue. Short arrays lose.
Tail handling grows. With 16 lanes you can have 15 leftover elements needing scalar treatment.
Frequency throttling. On several Intel generations, sustained heavy AVX-512 lowers the core clock — so the wide code runs fewer cycles per second. It can be a net loss, and this genuinely surprised people when AVX-512 shipped.
And most commonly: memory bandwidth binds first. If your loop is streaming through memory, you are limited by how fast data arrives (4.4), not by how many lanes you could process. Doubling vector width does nothing when you are waiting for DRAM.
You see xmm0 used throughout a function that does floating-point maths on single
values, with no arrays anywhere.
Explain why that is expected rather than surprising. Then say which specific part of the instruction names would tell you whether any real SIMD is happening.
What you should now be able to say
- Vectors live in a separate register file, shared with scalar float on x86.
- The name is the width:
xmm128,ymm256,zmm512, NEONv128. - Suffixes are systematic:
ss/sdscalar,ps/pdpacked;paddb/w/d/qby element size;vprefix means three-operand AVX. - Scalar float uses
xmm, soxmmalone does not mean SIMD. - Horizontal operations are expensive — keep results in lanes, combine once.
- Wider is not always better: data volume, tails, frequency, and memory bandwidth all interfere.