04 — matmul: Blocking & Packing (optim9 → optim14)¶
The 4×4 micro-kernel from tutorial 03 is compute-ready, but it still reads A and
B in cache-unfriendly patterns. This half of the ladder fixes that.
optim9–10 — pointer access + reordering¶
Pointer-based A access removes the per-element lda multiply; statement reordering
gives the compiler better instruction scheduling. Numbers don't change — only when
independent work is issued.
optim11 — SSE vectorization¶
Two rows of the tile are processed per 128-bit register:
Eight vector accumulators replace sixteen scalar ones. (This level uses x86 SSE
intrinsics and needs a local sse_compat.h.)
optim12 — cache blocking (mc × kc)¶
Block the p- and m-loops into panels so the working set fits in cache:
#define mc 256
#define kc 128
for (p = 0; p < k; p += kc) // k-panels
for (i = 0; i < m; i += mc) // mc×kc blocks
InnerKernel_(ib, n, pb, &A(i,p), &B(p,0), &C(i,0));
This is what pushes matmul from memory-bound to compute-bound: every panel element is reused from cache instead of DRAM.
optim13 — pack A¶
PackMatrixA copies each 4×k panel into a contiguous buffer so the kernel reads
A sequentially instead of chasing lda-strided columns:
optim14 — pack A and B, raw-pointer kernel¶
PackMatrixB packs B too, and the kernel reads both buffers with pure += 4
pointer steps. No stride arithmetic remains in the hot loop.
| Metric | optim8 | optim14 |
|---|---|---|
A access |
strided | packed, sequential |
B access |
strided | packed, sequential |
| accumulators | 16 registers | 16 (vectorized: 8) |
| working set | whole matrices | blocked panels |
Run ./build/matmat10 (the highest portable matmat level in the default build) and
compare its GFLOPS with ./build/matmat8.
Next
See how the matrix-vector product has a different optimal kernel in 05 — matvec: the 4-output kernel.