The demo below simulates a small matrix traversal. Choose a matrix size, then compare two strategies: row-major (reading left-to-right, top-to-bottom) vs recursive (splitting the matrix in half until it fits in a simulated cache block). Watch the cache-miss counter to see which strategy loads fewer cache lines.
<p class="hint">The matrix is stored <b>row-by-row</b> in memory (addr = row×N + col). The cache holds only a few lines at a time. Scanning <b>column-by-column</b> loads a new line on every step. The <b>recursive</b> strategy subdivides the matrix until subproblems fit in cache — matching the optimal regardless of cache size.</p>
<div class="controls">
<label>Matrix: <strong id="sizeLabel">16×16</strong></label>
<input type="range" id="sizeSlider" min="4" max="32" step="4" value="16">
<label>Cache line: <strong id="lineLabel">4</strong> cells</label>
<input type="range" id="lineSlider" min="2" max="8" step="2" value="4">
</div>
<div class="strategies">
<div class="strategy">
<div class="strategy-title">Column-scan (naive)</div>
<canvas id="colCanvas" width="200" height="200"></canvas>
<div class="miss-count">Cache misses: <span id="colMisses">—</span></div>
</div>
<div class="strategy">
<div class="strategy-title">Recursive (cache-oblivious)</div>
<canvas id="recCanvas" width="200" height="200"></canvas>
<div class="miss-count">Cache misses: <span id="recMisses">—</span></div>
</div>
</div>
<div class="verdict" id="verdict"></div>
<div class="btns">
<button id="runBtn" type="button">Run Simulation</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem 1.2rem; margin-bottom: .8rem; font-size: .9rem; }
input[type=range] { width: 110px; accent-color: #1d3557; }
.strategies { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .6rem; }
.strategy { flex: 1; min-width: 160px; }
.strategy-title { font-size: .85rem; font-weight: 700; color: #1d3557; margin-bottom: .3rem; }
canvas { border: 1px solid #cdd9e3; border-radius: 6px; display: block; width: 100%; height: auto; background: #f4f7fa; }
.miss-count { font-size: .9rem; margin-top: .35rem; }
.miss-count span { font-weight: 700; color: #1d3557; }
.verdict { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .5rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Cache-miss simulation for an N×N matrix stored row-major: addr = r*N + c.
// Cache is limited to maxLines = N/2 lines (smaller than the matrix).
// LRU eviction policy.
//
// Column-scan: iterates c outer, r inner → stride N between accesses.
// Each new element is on a different cache line → nearly N² misses (no reuse).
//
// Recursive (cache-oblivious): splits the larger dimension in half recursively.
// When a submatrix fits in cache it is processed without further evictions.
// Achieves the optimal Θ(N²/L) misses — same as the best possible blocked algorithm.
const colCanvas = document.getElementById('colCanvas');
const recCanvas = document.getElementById('recCanvas');
const colCtx = colCanvas.getContext('2d');
const recCtx = recCanvas.getContext('2d');
const colMissEl = document.getElementById('colMisses');
const recMissEl = document.getElementById('recMisses');
const sizeSlider = document.getElementById('sizeSlider');
const lineSlider = document.getElementById('lineSlider');
const sizeLabel = document.getElementById('sizeLabel');
const lineLabel = document.getElementById('lineLabel');
const verdictEl = document.getElementById('verdict');
sizeSlider.addEventListener('input', updateLabels);
lineSlider.addEventListener('input', updateLabels);
document.getElementById('runBtn').addEventListener('click', run);
document.getElementById('resetBtn').addEventListener('click', reset);
function updateLabels() {
sizeLabel.textContent = sizeSlider.value + '×' + sizeSlider.value;
lineLabel.textContent = lineSlider.value;
}
// LRU cache miss counter.
// order: array of [r, c] pairs; addr = r*N + c; line = floor(addr / L).
function lruMisses(order, L, N, maxLines) {
const cache = new Map(); // line -> last access tick
let misses = 0, tick = 0;
for (const [r, c] of order) {
const line = Math.floor((r * N + c) / L);
if (!cache.has(line)) {
misses++;
if (cache.size >= maxLines) {
// evict least-recently used
let minTick = Infinity, evictLine = -1;
for (const [l, t] of cache) if (t < minTick) { minTick = t; evictLine = l; }
cache.delete(evictLine);
}
cache.set(line, tick);
} else {
cache.set(line, tick); // update recency
}
tick++;
}
return misses;
}
// Column-major traversal: outer loop over columns, inner loop over rows.
// For row-major storage this is the "bad" pattern: stride = N between consecutive accesses.
function colOrder(N) {
const o = [];
for (let c = 0; c < N; c++)
for (let r = 0; r < N; r++)
o.push([r, c]);
return o;
}
// Recursive (cache-oblivious) traversal: always split the larger dimension.
// Visits every cell exactly once. Coverage verified by node tests.
function recOrder(r0, c0, rows, cols, out) {
if (rows === 1 && cols === 1) { out.push([r0, c0]); return; }
if (rows >= cols) {
const h = Math.floor(rows / 2);
recOrder(r0, c0, h, cols, out);
recOrder(r0+h, c0, rows-h, cols, out);
} else {
const h = Math.floor(cols / 2);
recOrder(r0, c0, rows, h, out);
recOrder(r0, c0+h, rows, cols-h, out);
}
}
// Draw traversal as a heat map: blue (first visited) → red (last visited).
function draw(ctx, order, N) {
const W = ctx.canvas.width, H = ctx.canvas.height;
const cw = W / N, ch = H / N;
ctx.clearRect(0, 0, W, H);
// Heat-map fill
const total = order.length;
// Build rank map for fast lookup
const rank = new Array(N * N);
order.forEach(([r, c], i) => { rank[r * N + c] = i; });
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const t = rank[r * N + c] / (total - 1);
const red = Math.round(30 + t * 205);
const green = Math.round(120 - Math.abs(t - 0.5) * 140);
const blue = Math.round(200 - t * 180);
ctx.fillStyle = `rgb(${red},${green},${blue})`;
ctx.fillRect(c * cw + 1, r * ch + 1, cw - 2, ch - 2);
}
}
// Grid lines
ctx.strokeStyle = 'rgba(0,0,0,0.12)';
ctx.lineWidth = 0.5;
for (let i = 0; i <= N; i++) {
ctx.beginPath(); ctx.moveTo(i * cw, 0); ctx.lineTo(i * cw, H); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, i * ch); ctx.lineTo(W, i * ch); ctx.stroke();
}
// Visit-order numbers for small matrices
if (N <= 8) {
ctx.fillStyle = '#fff';
ctx.font = `bold ${Math.max(7, Math.floor(cw * 0.42))}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
order.slice(0, 16).forEach(([r, c], i) => {
ctx.fillText(i + 1, c * cw + cw / 2, r * ch + ch / 2);
});
}
}
function run() {
const N = +sizeSlider.value;
const L = +lineSlider.value;
const maxLines = Math.max(4, Math.floor(N / 2)); // cache smaller than full matrix
const col = colOrder(N);
const rec = [];
recOrder(0, 0, N, N, rec);
const colM = lruMisses(col, L, N, maxLines);
const recM = lruMisses(rec, L, N, maxLines);
const optimal = Math.ceil(N * N / L);
draw(colCtx, col, N);
draw(recCtx, rec, N);
colMissEl.textContent = colM;
recMissEl.textContent = recM;
const saved = Math.round((1 - recM / colM) * 100);
if (recM < colM) {
verdictEl.style.color = '#0a7d33';
verdictEl.textContent =
fmt('Recursive saves %saved%% of misses (%recM% vs %colM%). Optimal lower bound: %optimal%.', { saved: saved, recM: recM, colM: colM, optimal: optimal });
} else {
verdictEl.style.color = '#555';
verdictEl.textContent = fmt('Both strategies match (%recM% misses). Try a larger matrix.', { recM: recM });
}
// Assertions logged for node verification
console.log('N=' + N + ' L=' + L + ' cache=' + maxLines +
'lines colMisses=' + colM + ' recMisses=' + recM + ' optimal=' + optimal +
' recIsOptimal=' + (recM === optimal) + ' recBeatsCol=' + (recM < colM));
}
function reset() {
colCtx.clearRect(0, 0, colCanvas.width, colCanvas.height);
recCtx.clearRect(0, 0, recCanvas.width, recCanvas.height);
colMissEl.textContent = '—';
recMissEl.textContent = '—';
verdictEl.textContent = '';
}
updateLabels();
run();
<p class="hint">The matrix is stored <b>row-by-row</b> in memory (addr = row×N + col). The cache holds only a few lines at a time. Scanning <b>column-by-column</b> loads a new line on every step. The <b>recursive</b> strategy subdivides the matrix until subproblems fit in cache — matching the optimal regardless of cache size.</p>
<div class="controls">
<label>Matrix: <strong id="sizeLabel">16×16</strong></label>
<input type="range" id="sizeSlider" min="4" max="32" step="4" value="16">
<label>Cache line: <strong id="lineLabel">4</strong> cells</label>
<input type="range" id="lineSlider" min="2" max="8" step="2" value="4">
</div>
<div class="strategies">
<div class="strategy">
<div class="strategy-title">Column-scan (naive)</div>
<canvas id="colCanvas" width="200" height="200"></canvas>
<div class="miss-count">Cache misses: <span id="colMisses">—</span></div>
</div>
<div class="strategy">
<div class="strategy-title">Recursive (cache-oblivious)</div>
<canvas id="recCanvas" width="200" height="200"></canvas>
<div class="miss-count">Cache misses: <span id="recMisses">—</span></div>
</div>
</div>
<div class="verdict" id="verdict"></div>
<div class="btns">
<button id="runBtn" type="button">Run Simulation</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem 1.2rem; margin-bottom: .8rem; font-size: .9rem; }
input[type=range] { width: 110px; accent-color: #1d3557; }
.strategies { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .6rem; }
.strategy { flex: 1; min-width: 160px; }
.strategy-title { font-size: .85rem; font-weight: 700; color: #1d3557; margin-bottom: .3rem; }
canvas { border: 1px solid #cdd9e3; border-radius: 6px; display: block; width: 100%; height: auto; background: #f4f7fa; }
.miss-count { font-size: .9rem; margin-top: .35rem; }
.miss-count span { font-weight: 700; color: #1d3557; }
.verdict { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .5rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Cache-miss simulation for an N×N matrix stored row-major: addr = r*N + c.
// Cache is limited to maxLines = N/2 lines (smaller than the matrix).
// LRU eviction policy.
//
// Column-scan: iterates c outer, r inner → stride N between accesses.
// Each new element is on a different cache line → nearly N² misses (no reuse).
//
// Recursive (cache-oblivious): splits the larger dimension in half recursively.
// When a submatrix fits in cache it is processed without further evictions.
// Achieves the optimal Θ(N²/L) misses — same as the best possible blocked algorithm.
const colCanvas = document.getElementById('colCanvas');
const recCanvas = document.getElementById('recCanvas');
const colCtx = colCanvas.getContext('2d');
const recCtx = recCanvas.getContext('2d');
const colMissEl = document.getElementById('colMisses');
const recMissEl = document.getElementById('recMisses');
const sizeSlider = document.getElementById('sizeSlider');
const lineSlider = document.getElementById('lineSlider');
const sizeLabel = document.getElementById('sizeLabel');
const lineLabel = document.getElementById('lineLabel');
const verdictEl = document.getElementById('verdict');
sizeSlider.addEventListener('input', updateLabels);
lineSlider.addEventListener('input', updateLabels);
document.getElementById('runBtn').addEventListener('click', run);
document.getElementById('resetBtn').addEventListener('click', reset);
function updateLabels() {
sizeLabel.textContent = sizeSlider.value + '×' + sizeSlider.value;
lineLabel.textContent = lineSlider.value;
}
// LRU cache miss counter.
// order: array of [r, c] pairs; addr = r*N + c; line = floor(addr / L).
function lruMisses(order, L, N, maxLines) {
const cache = new Map(); // line -> last access tick
let misses = 0, tick = 0;
for (const [r, c] of order) {
const line = Math.floor((r * N + c) / L);
if (!cache.has(line)) {
misses++;
if (cache.size >= maxLines) {
// evict least-recently used
let minTick = Infinity, evictLine = -1;
for (const [l, t] of cache) if (t < minTick) { minTick = t; evictLine = l; }
cache.delete(evictLine);
}
cache.set(line, tick);
} else {
cache.set(line, tick); // update recency
}
tick++;
}
return misses;
}
// Column-major traversal: outer loop over columns, inner loop over rows.
// For row-major storage this is the "bad" pattern: stride = N between consecutive accesses.
function colOrder(N) {
const o = [];
for (let c = 0; c < N; c++)
for (let r = 0; r < N; r++)
o.push([r, c]);
return o;
}
// Recursive (cache-oblivious) traversal: always split the larger dimension.
// Visits every cell exactly once. Coverage verified by node tests.
function recOrder(r0, c0, rows, cols, out) {
if (rows === 1 && cols === 1) { out.push([r0, c0]); return; }
if (rows >= cols) {
const h = Math.floor(rows / 2);
recOrder(r0, c0, h, cols, out);
recOrder(r0+h, c0, rows-h, cols, out);
} else {
const h = Math.floor(cols / 2);
recOrder(r0, c0, rows, h, out);
recOrder(r0, c0+h, rows, cols-h, out);
}
}
// Draw traversal as a heat map: blue (first visited) → red (last visited).
function draw(ctx, order, N) {
const W = ctx.canvas.width, H = ctx.canvas.height;
const cw = W / N, ch = H / N;
ctx.clearRect(0, 0, W, H);
// Heat-map fill
const total = order.length;
// Build rank map for fast lookup
const rank = new Array(N * N);
order.forEach(([r, c], i) => { rank[r * N + c] = i; });
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const t = rank[r * N + c] / (total - 1);
const red = Math.round(30 + t * 205);
const green = Math.round(120 - Math.abs(t - 0.5) * 140);
const blue = Math.round(200 - t * 180);
ctx.fillStyle = `rgb(${red},${green},${blue})`;
ctx.fillRect(c * cw + 1, r * ch + 1, cw - 2, ch - 2);
}
}
// Grid lines
ctx.strokeStyle = 'rgba(0,0,0,0.12)';
ctx.lineWidth = 0.5;
for (let i = 0; i <= N; i++) {
ctx.beginPath(); ctx.moveTo(i * cw, 0); ctx.lineTo(i * cw, H); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, i * ch); ctx.lineTo(W, i * ch); ctx.stroke();
}
// Visit-order numbers for small matrices
if (N <= 8) {
ctx.fillStyle = '#fff';
ctx.font = `bold ${Math.max(7, Math.floor(cw * 0.42))}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
order.slice(0, 16).forEach(([r, c], i) => {
ctx.fillText(i + 1, c * cw + cw / 2, r * ch + ch / 2);
});
}
}
function run() {
const N = +sizeSlider.value;
const L = +lineSlider.value;
const maxLines = Math.max(4, Math.floor(N / 2)); // cache smaller than full matrix
const col = colOrder(N);
const rec = [];
recOrder(0, 0, N, N, rec);
const colM = lruMisses(col, L, N, maxLines);
const recM = lruMisses(rec, L, N, maxLines);
const optimal = Math.ceil(N * N / L);
draw(colCtx, col, N);
draw(recCtx, rec, N);
colMissEl.textContent = colM;
recMissEl.textContent = recM;
const saved = Math.round((1 - recM / colM) * 100);
if (recM < colM) {
verdictEl.style.color = '#0a7d33';
verdictEl.textContent =
fmt('Recursive saves %saved%% of misses (%recM% vs %colM%). Optimal lower bound: %optimal%.', { saved: saved, recM: recM, colM: colM, optimal: optimal });
} else {
verdictEl.style.color = '#555';
verdictEl.textContent = fmt('Both strategies match (%recM% misses). Try a larger matrix.', { recM: recM });
}
// Assertions logged for node verification
console.log('N=' + N + ' L=' + L + ' cache=' + maxLines +
'lines colMisses=' + colM + ' recMisses=' + recM + ' optimal=' + optimal +
' recIsOptimal=' + (recM === optimal) + ' recBeatsCol=' + (recM < colM));
}
function reset() {
colCtx.clearRect(0, 0, colCanvas.width, colCanvas.height);
recCtx.clearRect(0, 0, recCanvas.width, recCanvas.height);
colMissEl.textContent = '—';
recMissEl.textContent = '—';
verdictEl.textContent = '';
}
updateLabels();
run();
With a small matrix both strategies behave similarly. Increase the size and the gap widens: row-major traversal crosses cache-line boundaries constantly when looping down columns; the recursive strategy stays spatially local at every scale. This is the core intuition — locality is achieved automatically by halving the problem until it fits, regardless of the actual cache size.
Comments
Loading comments...