Vector Processors

Picture a weather model adding two arrays of a million numbers each. A plain scalar CPU treats this as a million tiny dramas: load one number, load another, add them, store the result — then check the loop counter, test whether to branch back, and do it all again, a million times. Most of those instructions are pure bookkeeping. Only a third of them do arithmetic at all.

A vector processor looks at that same loop and says: why describe the same operation a million times? Say it once, over the whole array. A single vector instruction — \text{VADD } V3, V1, V2 — means "add these two vectors of 64 (or 256, or 1024) elements and put the answer in a third." One instruction fetch, one decode, one loop's worth of work gone, and a pipeline that hums along producing a result every clock cycle. This is the oldest and purest form of data-level parallelism: one instruction, many data.

The classic machine: registers that hold arrays

The archetype is the Cray-1 (1976), Seymour Cray's masterpiece. Where a scalar CPU has registers holding one number each, a vector machine adds vector registers, each holding a whole array — the Cray-1 had eight vector registers of 64 elements apiece. A vector instruction reads two of them, streams the elements through a deeply pipelined arithmetic unit, and writes a third.

Two special registers make it flexible. The vector length register (VLR) says how many of the elements are actually live — so a loop over 150 numbers runs as 64 + 64 + 22, the last pass with \text{VLR}=22 (this is strip mining). The vector stride register says how far apart in memory successive elements sit, so you can march down a column of a matrix (stride = row length) just as easily as a row (stride = 1).

How the speed actually happens: lanes and deep pipelines

A vector unit gets its throughput two ways at once. First, each arithmetic unit is deeply pipelined: a floating-point multiply might take 7 stages, but once the pipeline is full it delivers one finished product every cycle. Second, the elements are spread across several parallel lanes — each lane is its own pipeline handling every fourth element, say — so a 4-lane unit retires four results per cycle. Because the elements of a vector are known to be independent, the hardware never has to check for data hazards between them, which is exactly what lets it fill those pipelines fearlessly.

Chaining: forwarding, but for whole vectors

Suppose you compute V3 = V1 \times V2 and then immediately want V5 = V3 + V4. A naive machine waits for the entire multiply vector to finish before starting the add. Chaining instead forwards each product to the adder as soon as it emerges from the multiply pipeline — the two vector operations overlap almost completely, like register forwarding but sustained over a whole array. Chained together, a multiply and an add run in nearly the time of one. This is how vector machines hit their headline FLOP rates on kernels like the ubiquitous y = a\,x + y (SAXPY).

Say it once, not a million times

Here is the headline benefit made concrete. Take y_i = a\,x_i + y_i over N elements. The scalar loop runs a fistful of instructions per element — load, load, multiply, add, store, increment, compare, branch. The vector version, with 64-element registers, runs a handful of instructions per 64 elements. Run it and watch the instruction counts diverge:

// SAXPY: y[i] = a*x[i] + y[i], over N elements. // Compare instructions ISSUED by a scalar loop vs a 64-lane vector machine. const N = 1000; const VLEN = 64; // elements per vector register // Scalar: per iteration -> load x, load y, mul, add, store, add-index, cmp, branch = 8 instrs. const scalarInstrs = N * 8; // Vector: per strip of up to 64 elements -> set VLR, load Vx, load Vy, VMUL, VADD, store Vy, // bump pointer, branch = 8 instrs, but only once per 64 elements. const strips = Math.ceil(N / VLEN); const vectorInstrs = strips * 8; console.log(`N = ${N} elements`); console.log(`scalar loop: ${scalarInstrs} instructions issued`); console.log(`vector (64-wide): ${vectorInstrs} instructions issued (${strips} strips)`); console.log(`instruction-fetch reduction: ${(scalarInstrs / vectorInstrs).toFixed(1)}x`); console.log(`last strip runs with VLR = ${N % VLEN === 0 ? VLEN : N % VLEN}`);

Roughly a 60\times drop in instructions the front end must fetch and decode. The arithmetic work is identical — but the vector machine spends almost none of its energy on loop overhead, and its pipelines never stall waiting to discover the next iteration is independent.

The catch: it lives and dies by memory bandwidth

A vector unit that retires four adds per cycle needs eight input numbers and produces four outputs every cycle. Feeding it is the whole game. Classic vector supercomputers therefore skipped caches in favour of many banks of fast, heavily interleaved SRAM, and their designers obsessed over bandwidth, not latency. A vector kernel is almost always memory-bandwidth-bound: the arithmetic is cheap, and performance is set by how fast you can pour data in.

This is also why stride and gather/scatter matter so much. Unit-stride access (stride 1) streams cleanly across memory banks. A large stride can slam the same bank repeatedly and stall. And gather/scatter — using an index vector to fetch x[\,idx[i]\,] from scattered addresses, essential for sparse matrices — is the slowest access pattern of all, because the addresses are irregular and defeat the bank interleaving. Turning strided or indexed access into unit-stride is one of the oldest tricks in high-performance computing.

Through the 1980s a Cray was the supercomputer. What killed the pure vector machine wasn't a better idea — it was economics. Commodity microprocessors, riding the same manufacturing wave that made PCs cheap, got fast enough that a rack of them beat a bespoke vector machine per dollar (the "attack of the killer micros"). The vector idea, though, never died — it just moved. It reappeared as SIMD instructions bolted onto ordinary CPUs, as the lane-and-warp machinery inside every GPU, and, gloriously, in RISC-V's modern vector extension and ARM's SVE, which bring the Cray-style vector-length register back to phones and servers. The killer micros absorbed the vector, they didn't beat it.

It is tempting to think a 64-element vector register is only a fat integer you operate on all at once. But the defining feature is the vector length register: the same code runs correctly on arrays of 3, 64, or 150 elements without change, because the length is a runtime value, not baked into the instruction. That is precisely what packed-SIMD (MMX/SSE/AVX) got wrong for decades — those bake a fixed width into the opcode, so widening from 128 to 256 to 512 bits meant a whole new instruction set each time. The classic vector machine, and modern RISC-V V / ARM SVE, are "vector-length agnostic": write once, run at whatever width the hardware happens to have.

Vector vs scalar, at a glance

Scalar loopVector instruction
Instructions to add N numbers~8 per element~8 per strip of 64
Loop overheadpaid every elementamortised over the whole vector
Hazard checks between elementseach iterationnone — elements are independent
Bottleneckfront-end / controlmemory bandwidth
Best access patternanyunit-stride; gather/scatter is slow