Why Cache-Oblivious Divide-and-Conquer Matters
A matrix transpose performs almost no arithmetic, yet it can be slow because it moves data in an inconvenient order. Reading each source row consecutively writes a destination column with a large stride. Several writes may touch different cache lines before the program returns to the next word of any one destination line. If those lines have been evicted, the program fetches and writes them again.
Cache-oblivious divide-and-conquer organizes work into recursively smaller regions without consulting the cache capacity or line length. A depth-first traversal completes a small region and its descendants before moving elsewhere. This creates opportunities for locality at several scales, rather than selecting one hardware-specific tile size.
This lesson makes the idea concrete with an out-of-place matrix transpose. It is a representative algorithm, not a universal recipe for all recursive programs. You will implement the same traversal in Python, C++, and Java, trace every copy, and compare it with a row-major baseline using an explicit cache simulator. The matrix values stay the same; the order of accesses changes.
The distinction connects the earlier B/B+ tree lesson's page-transfer accounting with familiar divide-and-conquer reasoning from sorting. A cache line replaces a disk page as the transfer unit. Applications include reorganizing dense arrays, preparing data for numerical transforms, and designing recursive numerical kernels. The sample demonstrates transfer savings under stated assumptions; hardware speed still requires measurement.
Recursively split the longer matrix dimension and finish each child before its sibling. The traversal uses matrix shape and fixed storage strides, while the cache automatically retains useful lines. This can reduce data movement across several memory scales without a cache-size parameter in the algorithm.
Core Concepts and Terminology
The input is a flat row-major array $A$ of shape $r\times c$. The output is a separate flat row-major array $B$ of shape $c\times r$, satisfying $$ B_{ji}=A_{ij},\qquad 0\le i\lt r,\quad 0\le j\lt c. $$ Let $N=rc$. Source offset $ic+j$ becomes destination offset $jr+i$. The leading dimension, or row stride, is $c$ for A and $r$ for B. These strides remain fixed when recursion selects a smaller rectangle: a view does not repack the underlying array.
A recursive call describes a half-open source rectangle $[r_0,r_1)\times[c_0,c_1)$. Its height and width are $h=r_1-r_0$ and $w=c_1-c_0$. The corresponding output rectangle is $[c_0,c_1)\times[r_0,r_1)$. This interval representation handles odd dimensions and makes disjoint coverage easy to prove. The call's contract is to copy exactly this region to its transposed destination.
Two distinct roles for cache parameters. The transpose traversal knows only bounds, fixed strides, and how to load/store an element. Our observer has $\lambda$ words per cache line and $Q$ line frames, with data capacity $M=Q\lambda$ words. Changing $\lambda$ or $Q$ changes simulated traffic, but never changes the recursive decisions, leaf order, or result. The traversal never asks whether a rectangle fits in cache.
The classical ideal-cache model is fully associative and uses optimal offline replacement. Its tall-cache assumption is $M=\Omega(\lambda^2)$. For positive-size rectangular transpose, the classical bound is $\Theta(1+rc/\lambda)$ cache misses with $\Theta(rc)$ work; see Frigo, Leiserson, Prokop, and Ramachandran's Cache-Oblivious Algorithms, Section 3. Recursion alone does not establish such a bound: the layout and working-set analysis matter.
The executable laboratory uses LRU replacement. Every logical word is eight bytes for accounting, so a line transfers $8\lambda$ bytes and fast data capacity is $8Q\lambda$ bytes. These are modeled widths, not Python object sizes. Word address $a$ belongs to line $\lfloor a/\lambda\rfloor$ and slot $a\bmod\lambda$. Any line can occupy any cache frame; the least recently accessed resident line is evicted when all frames are occupied.
A missing load or store fetches one full line: this is write allocation. A store modifies its cached payload and marks the line dirty. Dirty eviction writes a full line to backing memory; clean eviction does not. At the end, a flush writes each remaining dirty line once and clears its dirty bit. We count $$ T=R+W_e+W_f, $$ where $R$ counts line reads, $W_e$ dirty eviction writes, and $W_f$ final flush writes. Hits are word references with no line fill. An eviction and a writeback are different events; a clean victim contributes only an eviction.
A starts at word zero. B starts at line-aligned address $$ p=\lambda\left\lceil\frac N\lambda\right\rceil. $$ The backing array has $2p$ words, including zero padding and initially zero B storage. Source setup, output initialization, and final inspection are outside the counted kernel; every kernel A load and B store goes through the simulated cache. Each comparison starts with a separate cold cache and finishes with a flush. Recursion metadata, instruction traffic, and trace storage are outside this data-cache model.
The diagram follows the first $2\times2$ source region of the sample. Its two source lines and two destination lines fit exactly in four frames. The program reaches this region by repeated splitting; it does not discover the fit with a capacity test.
| Term | Concrete meaning | Role |
|---|---|---|
| Cache oblivious | Split from $h,w$ without reading $M,\lambda$ | Keep traversal independent of cache geometry |
| Divide-and-conquer | Split the longer dimension; finish children depth first | Group work at several scales |
| View / half-open region | $[0,2)\times[0,2)$ in a $4\times4$ source | Select work without allocating a new matrix |
| Leading dimension | A stride $c$, B stride $r$ | Preserve correct global addresses inside every view |
| Cache line | Two logical words in the sample | Unit of read/write transfer |
| Capacity | $Q=4$, $\lambda=2$, $M=8$ words | Limit simultaneous resident payload |
| LRU | Least recent reference is the next victim | Define the finite simulator's replacement policy |
| Dirty / write allocate | First B store fetches a line and dirties it | Explain output fills and eventual writebacks |
| Ideal-cache analysis | Full associativity, optimal replacement, tall cache | State the asymptotic theorem's assumptions |
| Analysis cutoff | A conceptual level where a region fits | Prove locality while the program keeps recursing |
How Cache-Oblivious Divide-and-Conquer Works, Step by Step
- Validate the public input. Check dimensions, cache limits, source length, and values. Compute the aligned B base only after numeric limits are known. If either dimension is zero, return the empty transpose without entering recursion.
- Start at the full region. Call
visit(0,r,0,c,1). The final argument records depth, with the root at depth one. No data is moved merely by making the call. - Copy a scalar leaf. If $h=w=1$, load A at $r_0c+c_0$, then store the value at absolute B address $p+c_0r+r_0$. Record the source offset for the explanatory trace. The single scalar temporary is outside the modeled cache payload.
- Otherwise split the longer dimension. If $h\ge w$, use midpoint $m=r_0+\lfloor h/2\rfloor$ and recurse on the top then bottom source rectangle. Ties go to this row split. If $w\gt h$, use $m=c_0+\lfloor w/2\rfloor$ and visit the left then right rectangle. Both children retain the original A and B strides.
- Complete both children before returning. There is no numerical combine step: children have written disjoint output regions. The parent simply returns after both are finished. A child may reuse lines loaded by its sibling, depending on the cache and geometry.
- Flush before observing output. A correct value can still reside only in a dirty cache payload. Flush the remaining dirty lines, then read the returned B values directly from backing memory outside the count.
A positive nonleaf always has a longer dimension at least two. Its floor midpoint lies strictly inside its interval, so both child rectangles are positive and strictly smaller. Guarding zero dimensions before the root prevents an empty interval from splitting forever.
For the diagram's $3\times5$ source, the first split is along columns at two: children have shapes $3\times2$ and $3\times3$. The left child's output is B rows zero and one, while the right child's output is B rows two through four. The left child next splits its three rows at one. The actual code uses a $1\times1$ base case throughout.
A fixed small leaf cutoff independent of cache geometry can preserve cache obliviousness while reducing recursive overhead. A cutoff chosen from the target cache's $M$ or $\lambda$ is an explicit cache-aware choice. Neither changes the requirement to prove coverage and preserve the original strides.
flowchart TD
A["Validate input and align separate output"] --> B{"Either dimension zero?"}
B -->|Yes| J["Flush and return output"]
B -->|No| C["Call Visit on full rectangle; wait for it to return"]
C --> J
C -.-> D{"Visit: one row and one column?"}
D -->|Yes| E["Load A element and store transposed B element"]
D -->|No| F{"Height at least width?"}
F -->|Yes| G["Split rows; recursively Visit top then bottom"]
F -->|No| H["Split columns; recursively Visit left then right"]
G --> I["Return from this Visit call to its caller"]
H --> I
E --> I
Worked Example with a Full Trace
Use $$ A=\begin{bmatrix}1&2&3&4\\5&6&7&8\\9&10&11&12\\13&14&15&16\end{bmatrix}, \qquad r=c=4,\quad\lambda=2,\quad Q=4. $$ A occupies addresses $0$ through $15$, lines $0$ through $7$. B starts at $p=16$ and occupies addresses $16$ through $31$, lines $8$ through $15$. A two-word line is sixteen logical bytes; four frames hold sixty-four bytes.
All recursive regions. The root's row split creates top $[0,2)\times[0,4)$ and bottom $[2,4)\times[0,4)$. Each splits its columns at two. Thus the four $2\times2$ regions are visited top-left, top-right, bottom-left, bottom-right. Each splits its two rows into single rows; each resulting $1\times2$ region splits its two columns into leaves. These are all internal calls: one root, two $2\times4$ calls, four $2\times2$ calls, and eight $1\times2$ calls. There are sixteen leaves, thirty-one calls total, and maximum depth five.
The source offsets are visited in this exact order: $$ 0,1,4,5,\;2,3,6,7,\;8,9,12,13,\;10,11,14,15. $$ The diagram records every copied value and destination offset. Each leaf performs its A load before its B store. M/H means that the source load misses and the destination store hits; M/M means both cause line fills. The reads and writes columns are cumulative before final flush.
Cache state after every leaf. The following independent replay gives line IDs from least to most recently accessed. A star marks a dirty B line. Each event lists its line ID; any victim is shown in parentheses, with a star when its eviction writes a dirty payload. Destination line IDs are absolute, including B's base. This makes all thirty-two references and all twelve evictions reconstructible.
| Copy | Load event | Store event | Cache after store: LRU to MRU |
|---|---|---|---|
| 1 | M 0 | M 8 | 0, 8 dirty |
| 2 | H 0 | M 10 | 8 dirty, 0, 10 dirty |
| 3 | M 2 | H 8 | 0, 10 dirty, 2, 8 dirty |
| 4 | H 2 | H 10 | 0, 8 dirty, 2, 10 dirty |
| 5 | M 1 (evict 0 clean) | M 12 (evict 8 dirty) | 2, 10 dirty, 1, 12 dirty |
| 6 | H 1 | M 14 (evict 2 clean) | 10 dirty, 12 dirty, 1, 14 dirty |
| 7 | M 3 (evict 10 dirty) | H 12 | 1, 14 dirty, 3, 12 dirty |
| 8 | H 3 | H 14 | 1, 12 dirty, 3, 14 dirty |
| 9 | M 4 (evict 1 clean) | M 9 (evict 12 dirty) | 3, 14 dirty, 4, 9 dirty |
| 10 | H 4 | M 11 (evict 3 clean) | 14 dirty, 9 dirty, 4, 11 dirty |
| 11 | M 6 (evict 14 dirty) | H 9 | 4, 11 dirty, 6, 9 dirty |
| 12 | H 6 | H 11 | 4, 9 dirty, 6, 11 dirty |
| 13 | M 5 (evict 4 clean) | M 13 (evict 9 dirty) | 6, 11 dirty, 5, 13 dirty |
| 14 | H 5 | M 15 (evict 6 clean) | 11 dirty, 13 dirty, 5, 15 dirty |
| 15 | M 7 (evict 11 dirty) | H 13 | 5, 15 dirty, 7, 13 dirty |
| 16 | H 7 | H 15 | 5, 13 dirty, 7, 15 dirty |
Here dirty is the table's spelling of the star convention. For example, the first copy reads source line zero, fills destination line eight, and writes value one into its first slot. The second copy hits source line zero but fills destination line ten for value two. The third copy fills source line two, then hits destination line eight to add value five beside one. The fourth hits both lines and adds six beside two. Four line reads have now served eight word references.
At copy five, reading source line one evicts clean line zero; the store to line twelve evicts dirty line eight, writing the completed output pair $[1,5]$ to backing memory. Later copies follow the same ownership rule. At the end, resident lines are $[5,13,7,15]$ in LRU order; lines thirteen and fifteen are dirty. Final flush writes their pairs $[11,15]$ and $[12,16]$ and changes no recency order.
The complete transposed result is $$ B=\begin{bmatrix}1&5&9&13\\2&6&10&14\\3&7&11&15\\4&8&12&16\end{bmatrix}. $$ The recursive run has $32$ references, $16$ hits, $16$ reads, $6$ dirty eviction writes, and $2$ flush writes: $T=24$ transfers, or $384$ modeled bytes. The baseline reads A in flat order $0,1,\ldots,15$ and obtains exactly the same B. It has $8$ hits, $24$ reads, $13$ dirty eviction writes, and $3$ flush writes: $T=40$, or $640$ modeled bytes. Its twenty evictions exceed the recursive run's twelve. Both peak at four resident lines.
The saving is $16$ line transfers, or $40\%$ of the baseline's traffic. It is not a measured runtime improvement. Both versions still copy sixteen values and issue thirty-two modeled word references.
Trace It by Hand
Pause immediately after copy four of the sample. The cache is [0,8 dirty,2,10 dirty] from LRU to MRU. Predict the next source coordinate, source line, destination line, victims, and new traffic totals before reading further.
Reveal: the next source coordinate is $(0,2)$, value $3$. Its A address is $2$, line $1$; its B address is $16+2\cdot4=24$, line $12$. The load misses and evicts clean line $0$. The store misses and evicts dirty line $8$, writing pair $[1,5]$. The new cache is [2,10 dirty,1,12 dirty]. Cumulative reads rise from $4$ to $6$, dirty eviction writes from $0$ to $1$, and hits stay at $4$.
Repeat with only two frames. The same recursive leaf order remains, but both algorithms now take $40$ transfers. With eight frames, both take $24$. Explain why a fixed traversal can have different locality results as cache capacity changes, and why cache obliviousness does not promise a strict improvement on every configuration.
Implementation in Python, C++, and Java
Each panel is a complete standalone program with the same sample, traversal, write-back cache, and standard output. Run Python normally; compile C++ as C++17; save the Java panel as Main.java and run Main. The programs print separate line reads, dirty eviction writes, final flush writes, and their transfer sum. They also expose every recursive source offset.
Function contract. Supply A as a flat integer list/vector/array of exactly $rc$ elements. Dimensions must satisfy $0\le r,c\le64$; values lie in $[-10^6,10^6]$; $1\le\lambda\le64$ and $1\le Q\le128$. Python's final recursive argument defaults to True; C++ has the same default; Java requires the boolean argument explicitly. False selects the row-major baseline. The source is unchanged, and a separate result contains B and all counters.
Invalid dimension limits produce DIMENSION, invalid cache limits PARAMETER, wrong flat length SHAPE, and out-of-range values VALUE. Checks occur in that order before dimension multiplication or address arithmetic. Python first rejects non-exact integer parameter types and a non-exact boolean mode as PARAMETER; it rejects booleans, floats, strings, and other non-integer element types as VALUE. C++ uses typed int parameters, bool mode, and long long words; Java uses int, boolean, and long. The contract expects a non-null array/container; null or unrelated container objects are outside it.
Bounds keep all address/count arithmetic small, and values are copied without arithmetic. No matrix-size-dependent allocation occurs inside the recursive traversal. Python uses integer lists, C++ vectors, and Java primitive arrays; each cache line holds an actual copy of its backing payload. A store updates that payload, so forgetting dirty writeback would corrupt the observed output rather than merely change a counter.
Read the kernel separately from the observer. The visit method/function only reads interval lengths and divides them. The cache object receives $\lambda,Q$, and the initial storage setup uses $\lambda$ for alignment. Neither value participates in splitting or the scalar stopping rule. The source-order trace and call statistics remain identical for every allowed cache geometry. They are teaching instrumentation, not a requirement of a production transpose.
Zero dimensions produce an empty B with zero accesses, calls, evictions, writes, and peak resident lines. A singleton has one leaf call at depth one, one load, and one store; with only one frame, the source line is evicted before the distinct destination line can be fetched. Stores still allocate a line even though B was initially zero: that policy is part of the experiment.
Expected standard output, identical in all three languages:
shape=4x4 -> 4x4 line_words=2 cache_lines=4line_bytes=16 cache_bytes=64 equal=trueB=[1,5,9,13,2,6,10,14,3,7,11,15,4,8,12,16]recursive_source_order=[0,1,4,5,2,3,6,7,8,9,12,13,10,11,14,15]row_major accesses=32 hits=8 reads=24 eviction_writes=13 flush_writes=3 transfers=40 evictions=20 peak_lines=4 calls=0 max_depth=0recursive accesses=32 hits=16 reads=16 eviction_writes=6 flush_writes=2 transfers=24 evictions=12 peak_lines=4 calls=31 max_depth=5
Complexity, Correctness, and Invariants
Work, best/average/worst cases. For positive $N=rc$, every leaf copies one element and every internal call has exactly two children. There are $N$ leaves and $N-1$ internal calls, hence $$ \text{calls}=2N-1,\qquad U=2N,\qquad H+R=2N. $$ Here $U$ counts word references and $H$ hits. Work is $\Theta(N)$ in best, average, and worst cases for fixed dimensions; values do not affect traversal. There is no probabilistic average-case assumption for cache performance. The row-major baseline has the same $2N$ references, no recursive calls, and constant kernel control storage.
Each row split halves a positive row interval, and each column split halves a positive column interval. Root depth is one, so $$ \text{maximum depth}\le1+\lceil\log_2 r\rceil+\lceil\log_2 c\rceil. $$ The recursive control stack is $O(\log r+\log c)$ for positive dimensions. Calls execute sequentially: sibling subtrees do not both remain expanded on the stack. For an empty shape, kernel work/calls/traffic are zero, while validation/setup still has a fixed cost.
Exact traffic bounds for this simulator. For $N\gt0$, let $D=\lceil N/\lambda\rceil$. Distinct aligned A and B regions each touch $D$ lines. Every A line must be read, every B line must be allocated at its first store, and every B line must eventually be written back. Thus $$ R\ge2D,\qquad W_e+W_f\ge D,\qquad T\ge3D. $$ A writeback needs at least one store since that line's previous writeback or initial load. There are $N$ stores and $2N$ accesses, so $$ R\le2N,\qquad W_e+W_f\le N,\qquad 3D\le T\le3N. $$ These bounds include final flush and partial padded lines. A sufficient capacity for both orders to attain $3D$ is $Q\ge2D$, retaining all touched lines, but it is not necessary. In the sample, $D=8$ and the lower bound is $24$ transfers; recursion attains it with four frames even though full residency would require sixteen. With $\lambda=1$, the bounds meet at $3N$, and this model offers neither traversal a line-traffic advantage.
The sample's capacity sweep, with $r=c=4$ and $\lambda=2$, is:
| Frames $Q$ | Row-major transfers | Recursive transfers |
|---|---|---|
| 1 | 48 | 48 |
| 2 | 40 | 40 |
| 3 | 40 | 32 |
| 4 | 40 | 24 |
| 5 | 40 | 24 |
| 6 | 32 | 24 |
| 8 | 24 | 24 |
| 16 | 24 | 24 |
This shows both improvement and ties. The exact finite traffic depends on shape, alignment, replacement, write policy, and capacity; a single asymptotic expression is not a counter prediction.
Why recursive locality can be asymptotically optimal. For near-square regions, conceptually stop the analysis at side length $s$ on the order of $\sqrt M$, with a sufficiently small constant for headroom. Source plus destination occupy $O(s^2/\lambda+s)$ lines, including row fragments. Tall cache gives $s=\Omega(\lambda)$, making this $O(s^2/\lambda)$ and allowing the footprint to fit. Descendants continue executing, but finish inside that footprint. Summing over $O(N/s^2)$ such groups gives $O(N/\lambda)$ movement when both dimensions are sufficiently large; add the constant for a small whole input. Thin rectangles need a separate boundary argument, not an area-only fit claim. Prokop's thesis gives the full rectangular analysis.
The ideal-cache theorem counts misses under optimal replacement. Our finite numbers use LRU and explicitly include writes. LRU can be related to optimal replacement with constant-factor capacity comparisons, and regularity conditions support asymptotic transfer of suitable bounds; this does not make their same-capacity miss counts equal. In particular, arbitrary simulator settings need not meet the theorem's tall-cache assumptions. A write-allocating write-back execution with $O(1+N/\lambda)$ fills also has only that order of dirty writebacks, because each loaded line can be written back at most once before its next load, including final flush.
Space and simulation cost. Out-of-place output requires $\Theta(N)$ words. The demonstration additionally retains the input copy, padded backing memory, returned result, and $N$ trace indices: $O(N+\lambda)$ host storage excluding cache payload. Resident cache payload is at most $Q\lambda$ words, with $O(Q)$ replacement metadata, plus the logarithmic control stack. Trace arrays and metadata are outside the modeled data-cache capacity. With expected constant-time hash lookup, the software simulator costs $O(N+\lambda T)$ plus setup, because it copies a whole payload on each read/write transfer. Its timing is not the hardware timing of an uninstrumented transpose.
Correctness by region induction. A call on $[r_0,r_1)\times[c_0,c_1)$ must copy exactly the cells in that source rectangle to $[c_0,c_1)\times[r_0,r_1)$ in B. At a leaf, the global offset formulas write exactly the required value. A row split partitions the source into disjoint top/bottom rectangles and partitions the output into corresponding disjoint column regions. A column split partitions source columns and output rows. By induction, both children fulfill their contracts, and their union fulfills the parent's contract without overwriting another child's cell. The decreasing positive area proves termination. At the root, every output cell is written once.
Cache-coherence invariant. For any logical word, its newest value is in its dirty resident payload if present, otherwise in backing memory. Fetch preserves the current value; store modifies the authoritative resident payload; dirty eviction writes it before removal. Flush therefore materializes every B value before result inspection. Disjoint A/B regions ensure no output write destroys an unread source value. This memory invariant and the region proof establish correct values independently of the cache's hit rate.
| Method | Kernel work and control space | Movement / locality | Assumptions and tradeoffs |
|---|---|---|---|
| Row-major transpose | $\Theta(N)$; $O(1)$ control | Contiguous A, strided B; sample $40$ transfers | Simple baseline; destination lines can be repeatedly evicted |
| Cache-oblivious transpose | $\Theta(N)$; $O(\log r+\log c)$ stack | Ideal-cache $\Theta(1+N/\lambda)$ misses; sample $24$ transfers | Out-of-place arrays; tall cache and full associativity for theorem |
| Cache-aware tiled transpose | $\Theta(N)$; $O(1)$ loop control | Reuse a chosen input/output tile pair | Tile footprint tuned for target cache; tails and strides matter |
| Recursive with fixed scalar leaf kernel | $\Theta(N)$; logarithmic stack | Keeps multiple-scale grouping with less call overhead | Cutoff independent of cache geometry preserves obliviousness |
| In-place square transpose | $\Theta(N)$ with suitable swaps | Different address/ownership trace | Requires swap/diagonal handling; supplied copy routine is not sufficient |
| Packed / SIMD transpose kernel | $\Theta(N)$; extra packing/register state | Exchange small contiguous vector groups | Architecture and layout choices; overhead can dominate small cases |
Every call writes exactly the transposed image of its half-open source rectangle; its children partition that work into disjoint regions with unchanged global strides. Cached dirty payload remains authoritative until writeback, and final flush materializes it. Together these invariants preserve every value exactly once regardless of cache capacity or replacement decisions.
Variants, Pitfalls, and Optimizations
Preserve strides when splitting views. A $2\times2$ view inside a $3\times5$ A does not have source stride two. Using its width as the leading dimension reads unrelated cells. Keep original $c$ and $r$ in the global offset formulas; change only interval bounds. The diagram contrasts a correct source value six with a wrong value three caused by this mistake.
Handle odd and empty dimensions. Use half-open intervals and floor midpoints. The longer dimension of a positive nonleaf is at least two, so neither child is empty. Do not recurse at all when the original shape has a zero dimension. Tests like $3\times5$, $1\times64$, $64\times1$, and $0\times64$ expose assumptions hidden by square powers of two.
A recursive split does not copy a submatrix. Python slicing that constructs new matrices, or packing at every recursion level, adds allocation and memory movement beyond this model. Carry bounds and strides or a true noncopying view. An explicit depth-first stack can replace language recursion while preserving visit order; push the second child before the first when using a LIFO stack.
Use a suitable base case. Scalar leaves make the proof and trace simple but can be expensive in practice. A small fixed loop kernel reduces call overhead while keeping the traversal independent of $M,\lambda$. Selecting the cutoff by cache-size tuning makes that part cache aware. Vectorized or register-level transpose kernels can be useful leaves, but their reference trace and measured speed differ from the supplied scalar laboratory.
Out of place and in place are different contracts. The program writes into a separate aligned B region. Reusing the same storage overwrites unread values: for a $2\times2$ matrix, writing $B_{10}=A_{01}$ destroys the old $A_{10}$ before it is read. Square in-place transpose can swap pairs and treat diagonal blocks specially. Rectangular in-place transpose generally needs more careful permutation/cycle handling. Reusing the presented copy operation unchanged is incorrect.
Do not infer cache optimality from recursion alone. Balanced recursion can still access poor layouts or revisit too much data between uses. Long-stride row fragments affect the true line footprint; sufficient area in words does not by itself prove sufficient line capacity. Recursive FFT, matrix multiplication, and static recursive search layouts require their own dependency, layout, and cache analyses. Ordinary two-way mergesort is recursive, but that fact alone does not establish the optimal external-memory sorting bound; cache-oblivious sorting structures such as funnels need additional machinery.
Multilevel opportunity has assumptions. One recursive schedule contains regions at many sizes, which motivates reuse at more than one memory level. The classic multilevel results impose hierarchy and replacement conditions. Real caches have limited associativity, alignment effects, prefetch, TLB behavior, write buffers, and coherence traffic. Our one-level fully associative model omits all of those, including stack and instruction references. The theorem is not a universal guarantee for every machine.
Compare equivalent boundaries. Both runs begin cold, use the same logical layout, perform one load then one store per value, and flush at the same point. Omitting flush hides dirty output traffic. Changing to non-temporal stores, assuming an entire overwritten line needs no allocation, or counting initialization in only one run changes the experiment. Such policies can be valuable, but must be described and measured separately.
Measure useful outcomes. The $40\to24$ transfer change is a verified model result. A small scalar recursive Python implementation may still run slower than a simple loop because of call and object overhead. For production numerical work, compare uninstrumented kernels or established array-library operations with the actual layout, repeated measurements, and hardware counters where available. Check output first; cache efficiency cannot repair a wrong permutation.
Keep the original row strides, split positive half-open regions with interior midpoints, and guard empty input before recursion. Use distinct source/output storage and include dirty writeback in traffic counts. Cache-independent recursion and an ideal-cache bound do not promise faster execution or fewer transfers on every finite configuration.
Practice Problems and Real-World Use
Build an independent value oracle by enumerating B in row-major order: for each output row $j$, append $A_{0j},A_{1j},\ldots,A_{r-1,j}$. Do not use the implementation's recursive partition logic to compute expected values. Separately enumerate source offsets with an explicit stack and replay their load/store addresses using a timestamp-based recency oracle. This checks permutation correctness, split order, and cache accounting independently.
Useful properties include: the source stays unchanged; B has $rc$ values; the source-order trace is a permutation of $0,\ldots,N-1$ for positive N; recursive calls equal $2N-1$; every access is a hit or a line read; peak resident lines never exceed $Q$; and final backing output equals the direct transpose. Transposing twice with reversed shapes restores the original values, though its address trace and cache counters need not be the same. Changing values or cache geometry must not change this algorithm's recursive source order.
The supplied programs passed $3669$ shared inputs per language: $3654$ accepted cases and $15$ rejected cases. Tests compare every output value, leaf offset, call/depth statistic, and cache counter against independent flat-transpose, explicit-stack, and timestamp-cache oracles. They include exhaustive shapes from $0$ through $8$ in each dimension across several cache settings, randomized rectangles, odd and skinny shapes, $64\times64$ bounds, extreme allowed values, invalid numeric limits, bad lengths, and out-of-range values. Python additionally passed $24$ exact-type rejection checks, $3200$ generic load/store checks with backing-memory verification and idempotent flush, and $400$ comparisons of traversal independence across cache geometries. C++ ran with address and undefined-behavior sanitizers. All three standalone samples print byte-identical output.
In systems work, a materialized transpose can make a later traversal contiguous, but its cost must be amortized by that later work. A strided view alone can avoid the copy when consumers support it; a consumer requiring contiguous rows may justify materialization. Recursive tiling also informs image layout conversion and numerical kernels, but boundaries, dependencies, and the actual storage format still determine legality and benefit.
For deeper study, the original paper provides separate treatments of transpose, multiplication, FFT, and cache-oblivious sorting, while Prokop's thesis develops the model and analyses at greater length. Use the worked laboratory to understand exact traffic, then read those proofs to distinguish a portable traversal from a theorem about its memory behavior.
| Exercise or application | What it tests | Difficulty |
|---|---|---|
| Four-copy hand trace | Reproduce the first four LRU states and four cold line reads | Beginner |
| Odd $3\times5$ transpose | List all leaf offsets and preserve strides five and three | Beginner |
| Double transpose property | Recover A with reversed dimensions, including empty/skinny cases | Beginner |
| Cache-capacity sweep | Reproduce the transfer table and explain both ties and savings | Intermediate |
| Fixed-cutoff leaf kernel | Reduce recursive calls without consulting cache parameters | Intermediate |
| Layout conversion before a numerical kernel | Compare transpose cost with later contiguous-access savings | Intermediate |
| Recursive matrix multiplication / FFT | Prove legal data dependencies and derive a new working-set bound | Advanced |
| Cache-oblivious search/sorting structures | Study recursive layouts or funnels; explain why generic recursion is insufficient | Advanced |