JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Vectorization and the Power of BLAS

Your processor can do eight multiplications in the time it nominally does one, and there is a tuned library that already squeezes every drop out of that fact. Learn how SIMD vectorization and the three levels of BLAS turn the memory-bound nightmare of the last two guides into the compute-bound dream of matrix-matrix multiply.

One instruction, many numbers

The first guide of this rung gave you a slogan — flops are cheap, memory is slow — and the second showed you how the memory hierarchy and data locality decide whether your data is sitting ready in cache or trickling in from far-away DRAM. This guide is about the other side of the coin: even when the data IS ready, a naive loop wastes most of the arithmetic the chip is capable of. Modern processors do not add one pair of numbers at a time. They add a whole short vector of them at once.

This is SIMD — Single Instruction, Multiple Data. A SIMD register is wide: 256 bits holds four double-precision numbers, 512 bits holds eight. A single SIMD add instruction takes two such registers and produces four (or eight) sums in one shot, for roughly the same cost as one scalar add. The technique of arranging your computation to feed these wide instructions is vectorization, the SIMD vectorization you will lean on for the rest of this rung. The promise is blunt: a perfectly vectorized loop runs four or eight times faster than the scalar version, on the very same core, with no extra threads or hardware.

Why your handwritten loop leaves speed on the table

If vectorization is such a free win, why not just trust the compiler? Often you can — a clean loop adding two arrays auto-vectorizes happily. But the compiler is conservative, and many loops resist it. If iteration n depends on iteration n-1 (a recurrence), the chip cannot do four at once because it does not yet know the inputs. If your data is scattered in memory rather than contiguous, the wide load that fills a SIMD register would have to gather from four distant addresses, which is slow. If two pointers might secretly alias the same memory, the compiler must assume the worst and refuse. Each of these quietly drops you back to scalar speed.

Even when a loop DOES vectorize, raw SIMD only attacks the flop side of the ledger. Recall the arithmetic intensity from guide one — the ratio of flops performed to bytes moved from memory. Vectorizing makes the flops happen faster, but it does nothing to move bytes faster. On the roofline model, speeding up arithmetic just slides you rightward toward the slanted memory ceiling; if your kernel is memory-bound, you hit that ceiling and the SIMD units sit idle, starved for data. So vectorization alone is not the answer. The real prize is restructuring the computation to RAISE the arithmetic intensity — to do many flops per byte — and that is exactly the trick the BLAS pulls off.

BLAS: three levels of arithmetic intensity

The BLAS — Basic Linear Algebra Subprograms — is the standardized vocabulary that nearly all numerical software speaks underneath. It is not one library but an interface, with hand-tuned implementations (OpenBLAS, Intel MKL, Apple Accelerate, BLIS) racing to be fastest on each chip. The crucial design idea is that the routines come in three levels, and the level number tells you the arithmetic intensity. The whole reason the levels exist is to let you reach for the most data-efficient operation that solves your problem.

level   example          data      flops      intensity (flops/byte)
-----   ---------------   -------   --------   ----------------------
BLAS-1  y = a*x + y       O(n)      O(n)       ~ 1/2     (memory-bound)
BLAS-2  y = A*x + y       O(n^2)    O(n^2)     ~ 1/4     (memory-bound)
BLAS-3  C = A*B + C       O(n^2)    O(n^3)     ~ n/8     (compute-bound!)

The win: BLAS-3 reuses each loaded number O(n) times.
A 1000x1000 matrix multiply does ~10^9 flops but reads
only ~10^6 numbers -- ~1000 flops per number moved.
The three BLAS levels and their arithmetic intensity. Only BLAS-3 (matrix-matrix) breaks free of the memory ceiling, because flops grow like n^3 while data grows like n^2.

Read the table slowly, because it is the heart of high-performance numerical computing. Level 1 is vector-vector: scale a vector, add two vectors, take a dot product. It touches O(n) data and does O(n) flops, so it does about one flop per number loaded — hopelessly memory-bound. Level 2 is matrix-vector: it reads an entire n-by-n matrix, O(n^2) numbers, to do O(n^2) flops, so each matrix entry is used just once. Still memory-bound. Level 3 is matrix-matrix: it reads O(n^2) numbers but performs O(n^3) flops, so each number that arrives in cache gets reused about n times before being evicted.

That reuse is everything. The matrix-matrix product C = A*B + C — the routine traditionally called GEMM — is the one kernel where there are far more flops than bytes, so a clever implementation can keep the SIMD units fed and actually approach the chip's peak speed. It achieves this with exactly the blocking and tiling you met in guide two: partition the matrices into small blocks that fit in cache, multiply block-by-block, and reuse each block many times while it is warm. GEMM is where vectorization and data locality finally cooperate instead of fighting.

Casting your problem in terms of BLAS-3

Here is the practical lesson that reshapes how good numerical code is written: whenever you can, express your algorithm so most of its work lands in BLAS-3 matrix-matrix calls. This is why modern dense linear algebra is BLOCKED. Plain Gaussian elimination as you first learned it is a sea of scalar updates — essentially BLAS-1 and BLAS-2 — and runs at memory-bound speed. The same factorization, reorganized to update whole sub-blocks of the matrix at a time, turns the bulk of its arithmetic into GEMM calls and runs many times faster. Take the LU factorization A = L U you use to solve A x = b: the textbook version is slow, but a blocked rewrite is mostly GEMM.

  1. Start from that same LU factorization A = L U. The textbook version updates one row at a time: low arithmetic intensity, memory-bound.
  2. Partition A into a grid of blocks. Factor a thin panel of columns first using the slow scalar-ish code, but on a SMALL piece only.
  3. Use that factored panel to update the entire remaining trailing block of the matrix in one big C = C - A*B matrix-matrix multiply — a BLAS-3 GEMM.
  4. Repeat down the diagonal. The slow panel work is O(n^2) total; the fast GEMM work is O(n^3) — so the expensive part runs at compute-bound speed, and the whole solve flies.

This blocked design is precisely what LAPACK does. LAPACK is the higher-level library — solvers, factorizations, eigenvalue and least-squares routines — built ON TOP of the BLAS, and it is deliberately written so that its heavy lifting flows down into BLAS-3 calls. When you call a solver in NumPy, MATLAB, Julia, or R, you are almost always calling LAPACK calling tuned BLAS underneath. The whole BLAS and LAPACK stack is the reason a one-line solve in a high-level language can run at a large fraction of your hardware's peak.

Don't roll your own — and the honest fine print

The single most valuable habit this guide can leave you with: don't roll your own matrix kernels. A tuned GEMM represents person-years of work — cache blocking matched to the exact L1, L2, and L3 sizes, register tiling, SIMD intrinsics, prefetch hints, sometimes hand-written assembly — all to chase a speed your honest triple-nested loop will never see. The triple loop you would write from the definition typically runs at a few percent of peak; the library version runs near 90 percent. The don't-roll-your-own principle is not laziness — it is the correct engineering judgment that this problem is solved, and solved far better than you can in an afternoon.

Now the honest caveats, because BLAS is powerful but not magic. First, recasting in BLAS-3 does not change WHAT you compute, only how fast — and that 'what' is still floating-point. Different BLAS implementations may sum a dot product in a different order, and since floating-point addition is not associative, two correct libraries can give answers that differ in the last few bits. That is normal, not a bug; it is the same non-associativity you met when you first learned that 0.1 has no exact binary form. Second, all this glory is for DENSE matrices. A sparse matrix that is mostly zeros has low arithmetic intensity no matter how you block it — there simply are not enough flops to amortize the data movement — so sparse problems live closer to the memory ceiling and need the matrix-free and iterative methods of other rungs.

What to carry up the ladder

Pull the threads together. SIMD vectorization wins a factor of four or eight by doing many numbers per instruction inside one core, but it only speeds the arithmetic — it cannot beat the memory ceiling on its own. The escape is to raise arithmetic intensity, and the BLAS hands you a ladder of three levels to do exactly that, with BLAS-3 matrix-matrix multiply being the one compute-bound operation that can run near peak. Write your algorithms so the heavy work flows into BLAS-3 and LAPACK, and lean on libraries others spent years tuning.

Keep one honest perspective as you climb. Every speed-up here changes only how FAST you get an answer, never whether it is right — the result is still floating-point, still approximate, and a fast BLAS run of an ill-conditioned solve is just a quick way to reach a poor answer. Speed and accuracy are separate axes. In the next guides this same vectorize-and-reuse thinking scales outward: across many cores and machines with threads and message passing, and onto the GPU — where wide vectorization is the whole hardware philosophy and Amdahl's law sets the ceiling on what all that parallelism can buy you.