Block processing breaks large arrays into cache-sized blocks, dramatically improving performance for operations that access the same data multiple times.
Why block processing matters:
- L1 cache is typically 32-48 KB per core
- A 100,000-element double array = 800 KB — far exceeds L1
- Without blocking: each operation reads from main memory (slow)
- With blocking: data stays in L1 cache across operations (fast)
The BlockInit/BlockNext/BlockEnd pattern:
var a, b, c: Vector;
begin
a.Size(100000);
b.Size(100000);
// Fill a, b with data...
a.BlockInit; // set up block iteration for a
b.BlockInit; // set up block iteration for b
while not a.BlockEnd do
begin
c.Sin(a); // operates on current block of a
c.Add(b); // b's block is in cache alongside a's block
a.BlockNext; // advance to next block
b.BlockNext;
end;
end;
How it works:
BlockInit— records original size, sets Length to the globalMtxVecBlockSize(default 800 elements for double precision, 1600 for single)- Processing — all operations see only the block-sized view
BlockNext— shifts internal pointers to next block (zero-copy, like SetSubRange)BlockEnd— returns true when all blocks processed, restores original size
Block size selection: The block length is the global MtxVecBlockSize variable (unit Math387), preset to 800 elements for double precision and 1600 for single — about 6.25 KB either way, sized so 4-5 vectors fit together in a 32 KB L1 cache. Adjust MtxVecBlockSize to tune for a different cache size.
Common pitfalls:
- All arrays must be block-iterated together — if a and b are in the same loop, both need BlockInit/BlockNext/BlockEnd.
- Scalar reductions need accumulation:
// WRONG — only gets sum of last block
a.BlockInit;
while not a.BlockEnd do
begin
result := a.Sum; // overwrites on each block
a.BlockNext;
end;
// CORRECT — accumulate across blocks
total := 0;
a.BlockInit;
while not a.BlockEnd do
begin
total := total + a.Sum; // accumulate
a.BlockNext;
end;
- Output arrays are also blocked:
a.BlockInit;
c.BlockInit; // destination must also be blocked!
while not a.BlockEnd do
begin
c.Sin(a); // writes to current block of c
a.BlockNext;
c.BlockNext;
end;
When to use block processing:
- Arrays bigger than ~30 KB (roughly L1 cache size) AND multiple operations on same data
- Inner loops of algorithms where same vectors are accessed repeatedly
- Before multithreading — block processing is prerequisite for efficient threading
When not needed:
- Single operation on an array (Sin, Add, etc. already internally optimized)
- Small arrays that fit in cache
- Operations that only read data once (single-pass)