Where the flop count stopped predicting the time

The recursion that was never told the memory

A blocked elimination has to be tuned to its fast memory, and tuned to one memory it costs up to 2.9 times the best at another. A recursive elimination splits the columns in half down to one and reads no memory size at all. On eight fast memories from 36 to 576 words it moves between 0.94 and 1.28 times the words of the best tuned block, with the same 585,200 operations and the same pivots — and on a machine with two caches it beats the block tuned to either cache on six machines of seven.

Worth reading first: The same arithmetic at a different price · Elimination is a sequence of choices · A reduction that changes the order.

A block size is a property of the machine scanned a blocked LU factorisation over every integer block size at five fast memories and found the best block at exactly √M − 2, at all five. It is a strong result and a slightly uncomfortable one. A block size that belongs to the machine has to be told the machine, and a real machine has several fast memories nested inside each other, so the tuning has to pick one of them.

That essay ended by naming the way out without measuring it. A recursive elimination has no block size. It splits the columns in half, factors the left half, updates the right half, and factors what is left — doing each of those by splitting again, down to a single column. At some depth of that recursion every subproblem fits whatever the fast memory is, because the subproblems have every size between the whole matrix and one entry. The claim, stated in the memory-hierarchy literature as a theorem about asymptotic traffic, is that such a recursion gets the right behaviour without being told any of the sizes.

The theorem says nothing about constants, and the whole of the previous essay was about a constant — the factor of 1.56 between √(M/3) and √M − 2 — so the useful question is a measurement. On the same counted memory, on the same matrix, how many words does the recursion move against the best block the scan can find?

Words moved at every block size, and by the recursion that has none, n = 96, M = 144The blocked elimination's traffic against its block size on logarithmic axes, scanned at every integer from 1 to 24, with a minimum of 109,202 words at b = 10. The horizontal line is a recursive elimination on the same matrix and the same 144-word fast memory: it splits the columns in half down to one and was never told M, and it moves 126,742 words — 1.16 times the tuned block — with the same 585,200 operations, the same pivots and a residual of 2.9·10⁻¹⁶.110¹10²10⁵block size bwords movedbest block: b = 10the recursion: no block size126,742 wordsbest block, scanned1.1·10⁵the recursion1.3·10⁵recursion ÷ best1.2M = 144 words; the recursion never reads it1.16× the best of 24 blocks
Fig. 1 The blocked elimination’s traffic at every block size from 1 to 24 on a 96-by-96 matrix with 144 words of fast memory, with its minimum of 109,202 words at b = 10. The horizontal line is the recursive elimination on the same matrix and memory: never told M, it moves 126,742 words — 1.16 times the tuned block — with the same 585,200 operations, the same pivots and a residual of 2.9·10⁻¹⁶.

The same elimination, associated a third way

The recursion is not a different factorisation, and the first thing to check is that it is not. The same arithmetic at a different price established that the blocked and unblocked eliminations perform the same operations, associated differently, and return factors identical to the last bit. The recursion is a third association of the same set — the same move the order the products are taken in makes for a contraction of tensors, where one value has many evaluation orders and their costs differ by a factor of two million.

Its pivot search is partial pivoting over the whole column below the diagonal, and every interchange swaps whole rows exactly as the unblocked elimination does — the interchanges the swap that is not optional insists on — so the traffic of the swaps is common to all three and the comparison is of the update. By the time the recursion searches a column, every update from the columns to its left has been applied to it — the left half is always finished before the right half is touched — so the pivots are the unblocked elimination’s pivots. That is checked on the permutation rather than argued: the three permutation vectors are identical.

The operation count is compared as an integer, and it is the same integer: 585,200 multiply-adds and divisions for all three orderings at n = 96. The residual ‖PA − LU‖/‖A‖ is 2.9·10⁻¹⁶. Everything that differs between the three is in the column headed “words moved”, which is the point of having counted the memory rather than timed a machine: elimination is a sequence of choices, and none of the choices has changed.

The memory being counted is the model from the two earlier essays on blocking: a fast memory of M words, fully associative and least-recently-used, a slow memory of unlimited size, and a count of every word that has to cross between them. Nothing in the recursion reads M. The triangular solve and the Schur complement update inside it are recursive too, each halving whichever of its dimensions is larger, down to a single multiply-add. The only base case of the whole factorisation is the panel of one column.

Eight memories, and the recursion never knew which

The blocked elimination is tuned by brute force: at each memory the scan runs every block from 1 to 24 and keeps the minimum, which is the best any tuning restricted to that range can do. The recursion is run once per memory, with nothing to tune.

M best block its words recursion’s words ratio
36 3 209,874 254,959 1.215
64 6 158,119 201,919 1.277
100 8 124,598 151,802 1.218
144 10 109,202 126,742 1.161
196 12 94,778 113,585 1.198
256 14 88,877 97,472 1.097
400 10 76,423 73,727 0.965
576 12 64,319 60,258 0.937

Never more than 28% above the tuned block, and below it at the two largest memories. The worst case is M = 64, where the tuned block of 6 moves 158,119 words and the recursion 201,919.

Words moved at every block size, and by the recursion that has none, n = 96, M = 64The blocked elimination's traffic against its block size on logarithmic axes, scanned at every integer from 1 to 24, with a minimum of 158,119 words at b = 6. The horizontal line is a recursive elimination on the same matrix and the same 64-word fast memory: it splits the columns in half down to one and was never told M, and it moves 201,919 words — 1.28 times the tuned block — with the same 585,200 operations, the same pivots and a residual of 2.9·10⁻¹⁶.110¹10²10⁵block size bwords movedbest block: b = 6the recursion: no block size201,919 wordsbest block, scanned1.6·10⁵the recursion2·10⁵recursion ÷ best1.3M = 64 words; the recursion never reads it1.28× the best of 24 blocks
Fig. 2 The worst of the eight memories for the recursion, M = 64. The scan’s minimum is 158,119 words at b = 6; the recursion moves 201,919, 1.28 times as many. The recursion was run once and not tuned; the scan was run at twenty-four block sizes and its best kept.

The two wins need an honest reading, because they are partly the scan’s limitation rather than the recursion’s strength. At M = 400 and 576 the best block within 1 to 24 is 10 and 12, not the √M − 2 = 18 and 22 the previous essay’s rule predicts; the scan’s own minimum has stopped following √M at those memories, for a reason this measurement did not isolate. The recursion is bound by no rule of that kind. So the honest statement at the two largest memories is that the recursion beats every block this scan tried, not that it beats every schedule there is — a larger block, or a block that differs between the panel and the update, was not tried.

Words moved at every block size, and by the recursion that has none, n = 96, M = 576The blocked elimination's traffic against its block size on logarithmic axes, scanned at every integer from 1 to 24, with a minimum of 64,319 words at b = 12. The horizontal line is a recursive elimination on the same matrix and the same 576-word fast memory: it splits the columns in half down to one and was never told M, and it moves 60,258 words — 0.94 times the tuned block — with the same 585,200 operations, the same pivots and a residual of 2.9·10⁻¹⁶.110¹10²10⁵block size bwords movedbest block: b = 12the recursion: no block size60,258 wordsbest block, scanned6.4·10⁴the recursion6·10⁴recursion ÷ best0.94M = 576 words; the recursion never reads it0.94× the best of 24 blocks
Fig. 3 The largest memory, M = 576. The scan’s minimum is 64,319 words at b = 12; the recursion moves 60,258, 0.94 times as many. The scan’s best block is 12 where the √M − 2 rule would say 22, so the tuned elimination has itself stopped following its own rule here.

What the table does establish is the shape of the recursion’s cost. Across the six memories where the scan’s minimum is genuine, the recursion pays between 10% and 28% for not knowing M. That is a small price against what a wrong guess costs a blocked elimination: the previous essay’s table has a block chosen one column too large costing 2.7 times the best, and the two-cache measurement below has a block tuned to one memory costing up to 2.87 times the best at another. The recursion never pays the cliff, and it never collects the last 10 to 28 per cent of the slope.

A constant distance from the floor

A block size is a property of the machine also measured the blocked elimination against the lower bound on traffic for this model, n³/√M words for an n-by-n elimination, and found it sitting a constant above that floor across matrix sizes. The same floor is available for every row of the table above, and it separates the two orderings in a way the ratio column does not.

At n = 96 the floor is 884,736/√M words. Against it the tuned block moves 1.42, 1.43, 1.41, 1.48, 1.50, 1.61, 1.73 and 1.74 times the floor across the eight memories from 36 to 576 words — drifting upward by about a fifth as the memory grows, and taking a different block to do it at nearly every step. The recursion moves 1.73, 1.83, 1.72, 1.72, 1.80, 1.76, 1.67 and 1.63 times the floor: between 1.63 and 1.83 at every memory, with no drift.

That is the property the theorem about memory hierarchies states, seen at one size rather than as an asymptote. An ordering that stays within a fixed factor of the floor at every M without knowing M is exactly what having no block size is supposed to buy, and the factor here is about 1.75. The tuned block is closer to the floor on six memories of eight — by the ten to twenty-eight per cent of the ratio column — and further on the two where its own minimum stopped following √M.

Two cautions keep that reading honest. The floor is an asymptotic bound whose constant has not been established for this model, so “1.75 times the floor” is a ratio to a formula rather than to the best schedule that exists. And a flat ratio over a decade and a half of M at a single matrix size is evidence of a shape, not a proof of one. What the column does show is that the recursion’s cost is a property of the problem and the memory together, where the tuned block’s cost is also a property of how well it was tuned.

Why the recursion lands near the best block

The mechanism is visible in the shape of the blocked elimination’s curve, which is a slope on the left and a cliff on the right. A block slightly too small wastes a little; a block one column too large evicts the data the next inner loop is about to read and wastes a great deal.

The recursion’s subproblems halve at every level. For any fast memory there is a level at which a subproblem’s working set fits and the level above does not, and the subproblems at the level that fits are between half and all of the largest size that would fit. So the recursion is, in effect, always using a block on the safe side of the cliff, within a factor of two of the edge. On the slope side a factor of two is worth tens of per cent, which is the 10% to 28% in the table; on the cliff side it would be worth a factor of three, and the recursion is never on the cliff side because it only ever works at sizes that fit.

That reading is consistent with the table and is not separately verified by instrumenting the recursion’s levels. Two things in the table support it. The recursion’s ratio does not trend with M — 1.22 at 36, 1.28 at 64, 1.16 at 144, 1.10 at 256 — which is what a factor-of-two quantisation of a size against a smoothly moving edge should look like: a sawtooth rather than a drift. And the recursion’s absolute traffic falls at every step of the table, while the tuned block itself jumps — 14 at M = 256, then 10 at 400.

The same halving is what a reduction that changes the order used for a different purpose: a tall-skinny QR computed as a binary tree of small factorisations, where the tree is what lets a machine of sixteen processors touch the matrix once. Recursion and trees keep reappearing in this field for one reason. A problem divided in halves has a piece of every size, and a machine with a limit at an unknown size is served by whichever piece fits. The order decides the memory is the sparse field’s version of the same observation: one factorisation, many orderings, and a cost that belongs to the ordering alone.

A machine with two caches

The recursion’s case is weakest when there is one fast memory, because then a block can be tuned exactly and the recursion can only come close. Its case should be strongest when there are two, because then no single block can be tuned to both, and the recursion has not been tuned to either.

The counted memory here has two nested caches: an inner one of l₁ words inside an outer one of l₂ words, inside slow storage. Every touch refreshes both; an address missing from the inner cache is counted as a word crossing the inner boundary, and one missing from the outer cache as a word crossing the outer boundary as well. The two counts are kept separate rather than weighted, because a weight would be a guess about how much slower one boundary is than the other on some particular machine.

Words at two cache boundaries against the block size, caches of 64 and 576 words, n = 96Two curves on a logarithmic block axis, each the words moved across one cache boundary divided by that boundary's own minimum. The inner cache of 64 words is best served by b = 6, which costs 1.49 times the best at the outer boundary; the outer cache of 576 words is best served by b = 12, which costs 2.17 times the best at the inner one. The block that minimises the worse of the two is b = 6, at 1.49. The recursive elimination, drawn as two dots to the right because it has no block size, is at 1.28 and 0.94.110¹10²12345block size bwords ÷ that boundary's bestinner boundary, 64 wordsouter boundary, 576 wordsinner best: b = 6outer best: b = 12inner 1.28×outer 0.94×the recursionone block for both caches: b = 6, worse level 1.49×recursion 1.28×, told neither size
Fig. 4 Caches of 64 and 576 words at n = 96, each curve the words crossing one boundary divided by that boundary’s own minimum. The block tuned to the inner cache, b = 6, costs 1.49 times the best at the outer boundary; the block tuned to the outer cache, b = 12, costs 2.17 times the best at the inner. The recursion, drawn as two dots to the right because it has no block size, is at 1.28 and 0.94.

At caches of 64 and 576 words, the block tuned to the inner cache costs 1.49 times the best at the outer boundary, and the block tuned to the outer cache costs 2.17 times the best at the inner one. Whichever cache the tuning chose, the other one pays between a half and double. The recursion’s worse boundary is at 1.28, and its other is at 0.94.

Across seven pairs of cache sizes the block tuned to the inner cache costs 1.21 to 2.87 times the best at the outer boundary, and the block tuned to the outer costs 1.64 to 2.57 at the inner. The recursion’s worse boundary sits at 1.08 to 1.35 times its best. Against a block tuned to one cache the recursion wins on six of the seven machines. On the seventh, caches of 100 and 576 words, the inner cache’s own block of 8 happens to serve the outer cache too, at 1.21, against the recursion’s 1.22.

The block that knows both caches, and where it wins

There is a fairer opponent. A scan that is told both cache sizes can choose the block that minimises the worse of the two ratios — a compromise no single-cache tuning would find, and a block that often sits at neither cache’s own optimum. Against that block the recursion’s record is mixed, and the mixture is the finding.

inner, outer compromise block its worse ratio recursion’s worse ratio
36, 400 17 1.578 1.215
64, 576 6 1.486 1.277
36, 576 17 1.578 1.215
100, 576 8 1.208 1.218
64, 400 6 1.341 1.277
16, 256 7 1.242 1.348
25, 225 11 1.168 1.075

The recursion is better on five of the seven machines, within one per cent on one — 1.218 against 1.208 at caches of 100 and 576 — and worse on one, at caches of 16 and 256, where a block of 7 holds both boundaries within 1.24 and the recursion’s inner boundary is at 1.35.

Words at two cache boundaries against the block size, caches of 16 and 256 words, n = 96Two curves on a logarithmic block axis, each the words moved across one cache boundary divided by that boundary's own minimum. The inner cache of 16 words is best served by b = 2, which costs 2.87 times the best at the outer boundary; the outer cache of 256 words is best served by b = 14, which costs 1.99 times the best at the inner one. The block that minimises the worse of the two is b = 7, at 1.24. The recursive elimination, drawn as two dots to the right because it has no block size, is at 1.35 and 1.10.110¹10²1234block size bwords ÷ that boundary's bestinner boundary, 16 wordsouter boundary, 256 wordsinner best: b = 2outer best: b = 14inner 1.35×outer 1.10×the recursionone block for both caches: b = 7, worse level 1.24×recursion 1.35×, told neither size
Fig. 5 Caches of 16 and 256 words, the one machine of seven on which the compromise beats the recursion. The inner cache alone wants b = 2, which costs 2.87 at the outer boundary; the outer alone wants b = 14, which costs 1.99 at the inner; the block of 7 holds both at 1.24. The recursion is at 1.35 and 1.10.

The losing machine has the smallest inner cache, sixteen words, where the recursion’s factor-of-two quantisation is a factor of two on a very small size, and at that scale the slope side of the curve is steep. That is the same mechanism as the one-cache table’s worst case at M = 64, pushed further, and it is a prediction rather than an explanation until the recursion’s levels are counted directly.

Words at two cache boundaries against the block size, caches of 36 and 400 words, n = 96Two curves on a logarithmic block axis, each the words moved across one cache boundary divided by that boundary's own minimum. The inner cache of 36 words is best served by b = 3, which costs 2.20 times the best at the outer boundary; the outer cache of 400 words is best served by b = 10, which costs 1.69 times the best at the inner one. The block that minimises the worse of the two is b = 17, at 1.58. The recursive elimination, drawn as two dots to the right because it has no block size, is at 1.21 and 0.96.110¹10²1234block size bwords ÷ that boundary's bestinner boundary, 36 wordsouter boundary, 400 wordsinner best: b = 3outer best: b = 10inner 1.21×outer 0.96×the recursionone block for both caches: b = 17, worse level 1.58×recursion 1.21×, told neither size
Fig. 6 Caches of 36 and 400 words, a machine where the compromise is far from either cache’s own block. The inner cache wants b = 3 and the outer b = 10; the block that minimises the worse ratio is 17, at 1.58. The recursion is at 1.21 and 0.96, better than the compromise at both boundaries.

The refusal on this essay is the overstatement a reader might take from the headline, that the recursion moves fewer words than every tuned block. It does not. Against a block tuned to one memory with that memory known, it loses by 10% to 28% at six of eight memories. What it beats is the schedule tuned to one cache when there are two, on six machines of seven, and it roughly matches the schedule tuned to both without being told either size — which is the property a factorisation shipped in a library, to machines its authors will never see, actually needs.

What the memory model can and cannot say

The counts are exact integers on a model, and the model is the one the whole field has used: least-recently-used, fully associative caches with every word the same size and every boundary the same kind of boundary. A real cache is set-associative, has lines of eight or sixteen words, and is not least-recently-used, and a real machine’s second cache is shared between processes. The earlier essays measured the blocked elimination on this model precisely so that the comparison between orderings could be exact, and the same trade applies here: the ratios are properties of the orderings on the model, and not predictions of seconds on a machine.

The recursion’s cost is also larger in one respect the model does not count: the overhead of the recursion itself — function calls, index arithmetic, the bookkeeping of which half is being worked on — which is free on the model and not free on hardware. Recursive implementations commonly switch to an unblocked kernel below some small size for exactly that reason, and that switch is a block size again, chosen for the processor’s arithmetic rather than its cache.

And the model is sequential. The message and the word and memory bought with messages are the collection’s account of traffic between processors, where the quantity is messages rather than words and the lower bounds have a different shape; whether the recursion’s indifference to M carries over to indifference to the number of processors is a different question and is not asked here. The general model of memory without stated sizes, and its layouts and search trees, belongs to the study of algorithms rather than to this field, and nothing here re-derives it; what is measured is its numerical instance, on the one factorisation counted here.

Where this goes from here

Three levels, and the base case. The two-cache model is the smallest in which “tuned to which memory?” can be asked, and the recursion’s advantage should grow with the number of levels, since every added level is another memory a single block gets wrong. A three-level count, together with a recursion that switches to an unblocked kernel below a stated size, would measure the one block size a recursive code still has and how much it matters.

The panel. Every ordering here uses a one-column panel with partial pivoting over the whole column, which is the part of an elimination that neither blocking nor recursion changes. On a matrix much taller than it is wide the panel dominates the traffic, and the answer in the communication literature is a pivoting rule that works on a tree of blocks rather than on one column. That changes the pivots, and therefore the factorisation, and it is the first step on which the “same arithmetic” of these essays would stop being the same.

And recursion for QR. Householder QR has a recursive formulation of its own, with the same halving of columns and a compact representation of the accumulated reflections. Doing it twice and the tall-skinny tree measured QR’s traffic between processors; its traffic between caches, recursive against blocked, is unmeasured.

Shares its objects with

Essays that name at least two of the same things, and that neither author linked.

Named objects

A flat tag is an object no other essay names yet.

Blocked algorithmCache obliviousData movementFlop countLU factorisationMemory hierarchyPartial pivotingRecursive factorisation