Vector and SIMD ISA Extensions

So far every instruction we have met does one thing to one pair of operands: add x3, x1, x2 adds a single number to a single number. But an enormous share of real computing — graphics, audio, machine learning, physics, image filters — does the same operation to a whole array of numbers. Feeding those through one-at-a-time scalar instructions wastes the chip. The answer, baked into the ISA contract itself, is SIMD: Single Instruction, Multiple Data — one instruction that operates on many data elements at once.

The scalar loop we want to beat

Consider adding two arrays element by element, c[i] = a[i] + b[i]. Scalar code loops, doing one add per iteration:

; Scalar: one add per iteration — N adds, N loop overheads loop: lw t0, 0(a1) ; t0 = a[i] lw t1, 0(a2) ; t1 = b[i] add t2, t0, t1 ; ONE element added sw t2, 0(a3) ; c[i] = t2 addi a1, a1, 4 ; advance the three pointers addi a2, a2, 4 addi a3, a3, 4 addi a0, a0, -1 ; i-- bnez a0, loop ; repeat

For a million-element array that is a million trips through the loop. Yet each add is completely independent of the others — nothing stops us doing several at once. That untapped independence is called data-level parallelism, and SIMD is the ISA's way of exposing it.

Vector registers and lanes

A SIMD ISA adds wide registers that hold not one number but a packed vector of several. A 128-bit register can hold four 32-bit floats; the hardware slices it into four independent lanes and an add instruction adds all four pairs simultaneously, in the same time a scalar add takes one pair.

This is pure Single-Instruction-Multiple-Data: one opcode, multiple data elements. The width — how many lanes — is a defining property of the extension. Widen the register and you widen the throughput, at least until memory bandwidth or the supply of parallel work runs out.

The zoo of SIMD extensions

Almost every mainstream ISA has bolted SIMD on, growing the vector width over the years:

ExtensionISAVector widthStyle
SSEx86128-bit (4 floats)fixed width
AVX / AVX2x86256-bit (8 floats)fixed width
AVX-512x86512-bit (16 floats)fixed width
NEONARM128-bitfixed width
SVE / RVVARM / RISC-Vagnosticvector-length agnostic

Note the split. Older SIMD (SSE, AVX, NEON) hard-codes the width into the opcode — an AVX add is always 8 floats, so binaries must be recompiled to use a wider machine. The modern vector designs (ARM's SVE, RISC-V's RVV) are vector-length agnostic: the program asks the hardware "how long is your vector?" at run time and loops in chunks of that length, so the same binary runs on a 128-bit chip or a 1024-bit one and gets faster automatically. That is a genuinely different, and cleaner, ISA contract.

Scalar loop vs a 4-wide SIMD add

Here is the crux, in code. The scalar version adds one element per step; the "SIMD" version processes four lanes per step (unrolled to make the parallelism visible), so it needs a quarter of the iterations. On real hardware those four lane-adds happen in a single vector instruction — here we simulate the lanes with an inner loop and count how many vector operations each approach costs.

// Scalar vs 4-wide SIMD for c[i] = a[i] + b[i]. We COUNT the add-instructions issued. const a = [10, 20, 30, 40, 50, 60, 70, 80]; const b = [1, 2, 3, 4, 5, 6, 7, 8]; const N = a.length; const WIDTH = 4; // lanes per SIMD register (e.g. 128-bit / 32-bit floats) // --- Scalar: one add instruction per element --- const cScalar = new Array(N); let scalarAdds = 0; for (let i = 0; i < N; i++) { cScalar[i] = a[i] + b[i]; scalarAdds++; } console.log("scalar result :", cScalar.join(", ")); console.log("scalar add-instructions issued:", scalarAdds); // --- SIMD: one VECTOR add covers WIDTH lanes at once --- const cSimd = new Array(N); let vectorAdds = 0; for (let base = 0; base < N; base += WIDTH) { // In hardware this whole chunk is ONE vadd; the lanes run in parallel. for (let lane = 0; lane < WIDTH; lane++) { cSimd[base + lane] = a[base + lane] + b[base + lane]; } vectorAdds++; // one vector instruction per chunk of WIDTH } console.log("simd result :", cSimd.join(", ")); console.log(`vector add-instructions issued: ${vectorAdds} (WIDTH=${WIDTH})`); console.log(`instruction-count reduction: ${scalarAdds / vectorAdds}x fewer adds`);

Four times fewer add-instructions, and the same fourfold cut in loop overhead. The iron law reads this straight off: SIMD slashes the instruction count (one vector op replaces W scalar ops) while keeping CPI about the same — a rare factor you get to shrink without paying elsewhere, as long as the data really is parallel.

Where the vectors come from: auto-vectorisation

You rarely write SIMD by hand. Instead the compiler auto-vectorises: it recognises a loop whose iterations are independent — no iteration reads a value a later one writes — and rewrites it to process W elements per pass with vector instructions, plus a small scalar "tail" loop for the leftover elements when N is not a multiple of W. The catch is that the compiler must prove independence. A loop-carried dependence like a[i] = a[i-1] + 1, a function call it can't see into, or possible pointer aliasing (two pointers that might reference overlapping memory) all force it to fall back to safe scalar code. Writing vectorisable loops — simple, independent, alias-free — is a real skill precisely because the ISA can only exploit parallelism the compiler can see.

You hit three walls. First, memory bandwidth: a vector unit that can add 64 floats per cycle is useless if memory can only deliver 8 — you starve. Second, diminishing parallel work: Amdahl's law bites, since real loops have serial parts and awkward tail elements, and very wide vectors waste lanes on short arrays. Third — the reason AVX-512 was controversial — power and heat: firing up a huge 512-bit unit drew so much current that some Intel chips lowered their clock speed whenever AVX-512 ran, so code using it could paradoxically slow down neighbouring scalar code. Width is not free; it trades against bandwidth, utilisation and watts. The vector-length-agnostic designs are partly a reaction to this: let the hardware pick a width it can actually sustain.

Three different kinds of parallelism get muddled. SIMD is data parallelism within a single core: one instruction, one thread, many data lanes. Multicore / multithreading is task parallelism across cores: many instruction streams running at once. And pipelining overlaps stages of consecutive instructions. They are complementary — a modern chip uses all three at once — but they solve different problems and live at different ISA/microarchitecture layers. Saying "I made it parallel with AVX" is not the same as "I made it parallel with threads"; conflating them will send you optimising the wrong bottleneck.

The ISA cost of exposing parallelism

SIMD is a bargain, but not a free one, and the cost lands in the contract. Adding vectors means new architectural state (wide vector registers the OS must save and restore on a context switch), new instructions and encodings, rules for masking and tail handling, and — for fixed-width designs — a versioning headache as each new width (SSE → AVX → AVX-512) fragments the software ecosystem. That is the deep lesson of this whole module: the ISA is a contract, and every capability you expose to software — a register model, an addressing mode, a calling convention, a vector unit — is a promise you must keep forever. Choosing what to put in the contract, and what to leave to the compiler and microarchitecture, is the art of computer architecture. Vector and SIMD ideas carry straight into the data-parallelism and GPU material ahead, where these same lanes reappear by the thousand.