Below is a simulation of two execution strategies processing a column of integers and summing the ones that pass a filter. Both do the same arithmetic — the difference is all in how much overhead surrounds each value.
<!-- Controls for row count and batch size -->
<div class="controls">
<label>
Rows:
<input id="rowCount" type="range" min="1000" max="100000" step="1000" value="50000">
<span id="rowCountVal">50000</span>
</label>
<label>
Batch size:
<input id="batchSize" type="range" min="1" max="4096" step="1" value="1">
<span id="batchSizeVal">1</span>
</label>
</div>
<div class="chart-area">
<canvas id="chart" width="560" height="220"></canvas>
</div>
<div class="metrics" id="metrics"></div>
<div class="btns">
<button id="runBtn" type="button">Run simulation</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<p class="note">Simulation model: each operator call costs 80 ns of fixed overhead; each value costs 4 ns of real work. Batch size 1 = pure tuple-at-a-time. Higher batch sizes amortize the overhead across more values.</p>
/* Vectorized execution demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .7rem; }
label { display: flex; align-items: center; gap: .5rem; font-size: .88rem; color: #444; flex-wrap: wrap; }
input[type=range] { flex: 1; min-width: 120px; accent-color: #1d3557; }
span { font-weight: 700; color: #1d3557; min-width: 3.5rem; }
.chart-area { background: #f5f7fa; border: 1px solid #dde2e8; border-radius: 10px; padding: .5rem; margin-bottom: .7rem; }
canvas { display: block; max-width: 100%; }
.metrics { display: flex; gap: 1.2rem; flex-wrap: wrap; margin-bottom: .6rem; }
.metric { background: #e8eef3; border-radius: 8px; padding: .45rem .8rem; font-size: .85rem; }
.metric .val { font-weight: 700; font-size: 1.05rem; color: #1d3557; display: block; }
.metric .lbl { color: #666; font-size: .78rem; }
.metric.vec .val { color: #0a7d33; }
.metric.ratio .val { color: #b84800; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
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; }
.note { font-size: .8rem; color: #666; margin: 0; line-height: 1.4; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Cost model: every operator call pays a fixed overhead (function dispatch,
// null-check, type dispatch, loop setup). Real work per value is much smaller.
const OVERHEAD_PER_CALL_NS = 80; // fixed overhead per operator call, in nanoseconds
const WORK_PER_VALUE_NS = 4; // actual computation per value, in nanoseconds
const rowCountSlider = document.getElementById('rowCount');
const rowCountVal = document.getElementById('rowCountVal');
const batchSizeSlider = document.getElementById('batchSize');
const batchSizeVal = document.getElementById('batchSizeVal');
rowCountSlider.addEventListener('input', () => { rowCountVal.textContent = Number(rowCountSlider.value).toLocaleString(); });
batchSizeSlider.addEventListener('input', () => { batchSizeVal.textContent = Number(batchSizeSlider.value).toLocaleString(); });
// Simulate both strategies: tuple-at-a-time (batchSize=1) vs vectorized (batchSize=k)
function simulate(rows, batchSize) {
const calls = Math.ceil(rows / batchSize);
const overheadNs = calls * OVERHEAD_PER_CALL_NS;
const workNs = rows * WORK_PER_VALUE_NS;
const totalNs = overheadNs + workNs;
const overheadPct = (overheadNs / totalNs) * 100;
return { totalNs, overheadNs, workNs, overheadPct };
}
// Draw two stacked bars comparing overhead vs work for both strategies
function draw(tupleRes, vecRes) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const barW = 80;
const maxNs = Math.max(tupleRes.totalNs, vecRes.totalNs) * 1.1;
const scale = (H - 60) / maxNs;
function drawBar(x, res, label, colorWork, colorOverhead) {
const wH = res.workNs * scale;
const oH = res.overheadNs * scale;
const base = H - 40;
// draw overhead segment (top portion of bar)
ctx.fillStyle = colorOverhead;
ctx.fillRect(x, base - wH - oH, barW, oH);
// draw work segment (bottom portion of bar)
ctx.fillStyle = colorWork;
ctx.fillRect(x, base - wH, barW, wH);
ctx.fillStyle = '#333';
ctx.font = '12px system-ui';
ctx.textAlign = 'center';
ctx.fillText(label, x + barW / 2, H - 20);
const ms = (res.totalNs / 1e6).toFixed(1);
ctx.fillText(ms + ' ms', x + barW / 2, base - wH - oH - 6);
}
const gap = (W - 2 * barW) / 3;
drawBar(gap, tupleRes, 'Tuple-at-a-time', '#4a90d9', '#e63946');
drawBar(gap * 2 + barW, vecRes, 'Vectorized', '#4a90d9', '#f4a261');
// draw legend in top-right corner
ctx.font = '11px system-ui';
ctx.textAlign = 'left';
ctx.fillStyle = '#4a90d9'; ctx.fillRect(W - 160, 10, 14, 14); ctx.fillStyle = '#333'; ctx.fillText('Real work', W - 142, 22);
ctx.fillStyle = '#e63946'; ctx.fillRect(W - 160, 30, 14, 14); ctx.fillStyle = '#333'; ctx.fillText('Overhead (tuple)', W - 142, 42);
ctx.fillStyle = '#f4a261'; ctx.fillRect(W - 160, 50, 14, 14); ctx.fillStyle = '#333'; ctx.fillText('Overhead (vector)', W - 142, 62);
}
function run() {
const rows = parseInt(rowCountSlider.value);
const batchSize = parseInt(batchSizeSlider.value);
const tupleRes = simulate(rows, 1); // tuple-at-a-time always uses batch size 1
const vecRes = simulate(rows, batchSize);
draw(tupleRes, vecRes);
const speedup = tupleRes.totalNs / vecRes.totalNs;
const metrics = document.getElementById('metrics');
metrics.innerHTML = `
<div class="metric">
<span class="val">${(tupleRes.totalNs / 1e6).toFixed(1)} ms</span>
<span class="lbl">tuple-at-a-time</span>
</div>
<div class="metric vec">
<span class="val">${(vecRes.totalNs / 1e6).toFixed(1)} ms</span>
<span class="lbl">${i18nFmt('vectorized (batch=%b%)', { b: batchSize.toLocaleString() })}</span>
</div>
<div class="metric ratio">
<span class="val">${speedup.toFixed(1)}×</span>
<span class="lbl">speedup</span>
</div>
<div class="metric">
<span class="val">${vecRes.overheadPct.toFixed(1)}%</span>
<span class="lbl">overhead fraction (vectorized)</span>
</div>
`;
}
document.getElementById('runBtn').addEventListener('click', run);
document.getElementById('resetBtn').addEventListener('click', () => {
rowCountSlider.value = 50000;
batchSizeSlider.value = 1;
rowCountVal.textContent = '50,000';
batchSizeVal.textContent = '1';
document.getElementById('metrics').innerHTML = '';
const canvas = document.getElementById('chart');
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
});
// Run once on load so the chart appears immediately
run();
<!-- Controls for row count and batch size -->
<div class="controls">
<label>
Rows:
<input id="rowCount" type="range" min="1000" max="100000" step="1000" value="50000">
<span id="rowCountVal">50000</span>
</label>
<label>
Batch size:
<input id="batchSize" type="range" min="1" max="4096" step="1" value="1">
<span id="batchSizeVal">1</span>
</label>
</div>
<div class="chart-area">
<canvas id="chart" width="560" height="220"></canvas>
</div>
<div class="metrics" id="metrics"></div>
<div class="btns">
<button id="runBtn" type="button">Run simulation</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<p class="note">Simulation model: each operator call costs 80 ns of fixed overhead; each value costs 4 ns of real work. Batch size 1 = pure tuple-at-a-time. Higher batch sizes amortize the overhead across more values.</p>
/* Vectorized execution demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .7rem; }
label { display: flex; align-items: center; gap: .5rem; font-size: .88rem; color: #444; flex-wrap: wrap; }
input[type=range] { flex: 1; min-width: 120px; accent-color: #1d3557; }
span { font-weight: 700; color: #1d3557; min-width: 3.5rem; }
.chart-area { background: #f5f7fa; border: 1px solid #dde2e8; border-radius: 10px; padding: .5rem; margin-bottom: .7rem; }
canvas { display: block; max-width: 100%; }
.metrics { display: flex; gap: 1.2rem; flex-wrap: wrap; margin-bottom: .6rem; }
.metric { background: #e8eef3; border-radius: 8px; padding: .45rem .8rem; font-size: .85rem; }
.metric .val { font-weight: 700; font-size: 1.05rem; color: #1d3557; display: block; }
.metric .lbl { color: #666; font-size: .78rem; }
.metric.vec .val { color: #0a7d33; }
.metric.ratio .val { color: #b84800; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
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; }
.note { font-size: .8rem; color: #666; margin: 0; line-height: 1.4; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Cost model: every operator call pays a fixed overhead (function dispatch,
// null-check, type dispatch, loop setup). Real work per value is much smaller.
const OVERHEAD_PER_CALL_NS = 80; // fixed overhead per operator call, in nanoseconds
const WORK_PER_VALUE_NS = 4; // actual computation per value, in nanoseconds
const rowCountSlider = document.getElementById('rowCount');
const rowCountVal = document.getElementById('rowCountVal');
const batchSizeSlider = document.getElementById('batchSize');
const batchSizeVal = document.getElementById('batchSizeVal');
rowCountSlider.addEventListener('input', () => { rowCountVal.textContent = Number(rowCountSlider.value).toLocaleString(); });
batchSizeSlider.addEventListener('input', () => { batchSizeVal.textContent = Number(batchSizeSlider.value).toLocaleString(); });
// Simulate both strategies: tuple-at-a-time (batchSize=1) vs vectorized (batchSize=k)
function simulate(rows, batchSize) {
const calls = Math.ceil(rows / batchSize);
const overheadNs = calls * OVERHEAD_PER_CALL_NS;
const workNs = rows * WORK_PER_VALUE_NS;
const totalNs = overheadNs + workNs;
const overheadPct = (overheadNs / totalNs) * 100;
return { totalNs, overheadNs, workNs, overheadPct };
}
// Draw two stacked bars comparing overhead vs work for both strategies
function draw(tupleRes, vecRes) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const barW = 80;
const maxNs = Math.max(tupleRes.totalNs, vecRes.totalNs) * 1.1;
const scale = (H - 60) / maxNs;
function drawBar(x, res, label, colorWork, colorOverhead) {
const wH = res.workNs * scale;
const oH = res.overheadNs * scale;
const base = H - 40;
// draw overhead segment (top portion of bar)
ctx.fillStyle = colorOverhead;
ctx.fillRect(x, base - wH - oH, barW, oH);
// draw work segment (bottom portion of bar)
ctx.fillStyle = colorWork;
ctx.fillRect(x, base - wH, barW, wH);
ctx.fillStyle = '#333';
ctx.font = '12px system-ui';
ctx.textAlign = 'center';
ctx.fillText(label, x + barW / 2, H - 20);
const ms = (res.totalNs / 1e6).toFixed(1);
ctx.fillText(ms + ' ms', x + barW / 2, base - wH - oH - 6);
}
const gap = (W - 2 * barW) / 3;
drawBar(gap, tupleRes, 'Tuple-at-a-time', '#4a90d9', '#e63946');
drawBar(gap * 2 + barW, vecRes, 'Vectorized', '#4a90d9', '#f4a261');
// draw legend in top-right corner
ctx.font = '11px system-ui';
ctx.textAlign = 'left';
ctx.fillStyle = '#4a90d9'; ctx.fillRect(W - 160, 10, 14, 14); ctx.fillStyle = '#333'; ctx.fillText('Real work', W - 142, 22);
ctx.fillStyle = '#e63946'; ctx.fillRect(W - 160, 30, 14, 14); ctx.fillStyle = '#333'; ctx.fillText('Overhead (tuple)', W - 142, 42);
ctx.fillStyle = '#f4a261'; ctx.fillRect(W - 160, 50, 14, 14); ctx.fillStyle = '#333'; ctx.fillText('Overhead (vector)', W - 142, 62);
}
function run() {
const rows = parseInt(rowCountSlider.value);
const batchSize = parseInt(batchSizeSlider.value);
const tupleRes = simulate(rows, 1); // tuple-at-a-time always uses batch size 1
const vecRes = simulate(rows, batchSize);
draw(tupleRes, vecRes);
const speedup = tupleRes.totalNs / vecRes.totalNs;
const metrics = document.getElementById('metrics');
metrics.innerHTML = `
<div class="metric">
<span class="val">${(tupleRes.totalNs / 1e6).toFixed(1)} ms</span>
<span class="lbl">tuple-at-a-time</span>
</div>
<div class="metric vec">
<span class="val">${(vecRes.totalNs / 1e6).toFixed(1)} ms</span>
<span class="lbl">${i18nFmt('vectorized (batch=%b%)', { b: batchSize.toLocaleString() })}</span>
</div>
<div class="metric ratio">
<span class="val">${speedup.toFixed(1)}×</span>
<span class="lbl">speedup</span>
</div>
<div class="metric">
<span class="val">${vecRes.overheadPct.toFixed(1)}%</span>
<span class="lbl">overhead fraction (vectorized)</span>
</div>
`;
}
document.getElementById('runBtn').addEventListener('click', run);
document.getElementById('resetBtn').addEventListener('click', () => {
rowCountSlider.value = 50000;
batchSizeSlider.value = 1;
rowCountVal.textContent = '50,000';
batchSizeVal.textContent = '1';
document.getElementById('metrics').innerHTML = '';
const canvas = document.getElementById('chart');
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
});
// Run once on load so the chart appears immediately
run();
Comments
Loading comments...