Skip to content

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:

\[ \mathbf{\Gamma}_{0c} \mathrel{+}= \mathbf{a}_{0,1}(p)\cdot\mathbf{b}_c(p) \]

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:

\[ \texttt{packedA}[4p + r] = A(i+r, p) \]

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.