Complete Mathematics of "Optimization 6: The 4×4 Kernel (via AddDot calls)"¶
Optimization 6 expands the micro-kernel from a 1×4 row (Optimization 2–5) to a 4×4 block of \(C\). Rather than fusing everything at once, it builds the block by calling the original AddDot routine sixteen times — one per element of the \(4×4\) tile. This is deliberately a correctness-first structural step; the fused, register-bound version comes in Optimization 7–8.
1. The Mathematical Operation¶
For a \(4×4\) block of \(C\) located at rows \(i, i+1, i+2, i+3\) and columns \(j, j+1, j+2, j+3\):
In block form, with
this is the block-matrix product
2. Construction by Sixteen Dot Products¶
The code's AddDot4x4 fills the tile by calling AddDot sixteen times:
AddDot(k,&A(0,0),lda,&B(0,0),&C(0,0)); // (r=0,c=0)
AddDot(k,&A(0,0),lda,&B(0,1),&C(0,1)); // (r=0,c=1)
...
AddDot(k,&A(3,0),lda,&B(0,3),&C(3,3)); // (r=3,c=3)
Each AddDot computes a scalar dot product:
i.e. the standard row-\(\times\)-column dot product for every \((r,c)\).
3. Loop Structure¶
The outer loops now tile both dimensions:
for (j = 0; j < n; j += 4)
for (i = 0; i < m; i += 4)
AddDot4x4(k, &A(i,0), lda, &B(0,j), ldb, &C(i,j), ldc);
Number of tile calls:
Each tile performs \(16\) dots of length \(k\), so total dots \(= \frac{mn}{16}\cdot 16 = mn\), and total FLOPs remain \(2mnk\).
4. Data Reuse Introduced by the 4×4 Tile¶
The block formulation lets us count reuse per tile:
- A-panel: \(4\) rows of length \(k\) \(\Rightarrow\) \(4k\) elements, each read \(4\) times (once per column \(c\)).
- B-panel: \(4\) columns of length \(k\) \(\Rightarrow\) \(4k\) elements, each read \(4\) times (once per row \(r\)).
Per tile, the input is \(8k\) elements used to produce \(4\cdot4=16\) outputs, each requiring a length-\(k\) dot. This is the seed of the register-blocking idea: a \(4×4\) accumulator tile lets every loaded \(A\) element and every loaded \(B\) element be reused 4 times before leaving the registers.
5. Why It Regresses (and Why It's Still Valuable)¶
Calling AddDot sixteen times discards the register-caching of the accumulators (Optimization 4–5): each AddDot writes its result through memory via *gamma. So per tile:
- \(16\) function calls;
- \(16k\) loads + \(16k\) stores of \(C\) through memory (back to the Optimization 3 pattern);
- the same row of \(A\) (
&A(r,0)) is loaded once per column \(c\), i.e. \(4\) redundant traversals of each A row.
This version is therefore slower than Optimization 5 in practice. Its purpose is purely structural: it establishes the correct 4×4 output register tile that the subsequent, fused versions optimize. It is a stepping stone, not a performance peak.
6. Summary¶
| Metric | Optim 5 (1×4) | Optim 6 (4×4, byAddDot) |
|---|---|---|
| Kernel shape | \(1\times4\) | \(4\times4\) |
| \(C\) tile accumulators | 4 registers | 16 memory writes |
| FLOPs | \(2mnk\) | \(2mnk\) |
| per-element A reuse | 4 | 4 |
| per-element B reuse | 1 | 4 |
| Primary goal | performance | correctness + structure |
Mathematically, Optimization 6 is just the block product \(\mathbf{C} \mathrel{+}= \mathbf{A}_{4\times k}\mathbf{B}_{k\times4}\) decomposed into sixteen scalar dot products. Its real contribution is defining the 4×4 register tile that later versions fill efficiently.