σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsSkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
RoadmapsSkills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsSkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
RoadmapsSkills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Cache locality skill›Module 2 · Layout and traversal›Topic 2.2·~55 minIntermediate

Traversal order and loop tiling

Skill positionModule 2 of 3 · Topic 2 of 2
2.2 Traversal order and loop tiling
—Lab score— / 100Total XP—Streak—

5 min readCode examples: 4

  1. 01Which loop goes inside?
  2. 02When both loops disagree: the transpose
  3. 03What you will build

Two nested loops that visit the same cells of a matrix can differ by a factor of seven in cache misses just because of which loop is inside. This lecture compares reading row by row and column by column, then cuts a transpose into tiles so that a block stays in the cache while it is used, and measures both on a matrix made from SPY closes.

01

Which loop goes inside?

A matrix with n rows and n columns is stored in row-major order. A loop that moves along a row, changing j, reads neighbouring values. A loop that moves down a column, changing i, jumps a whole row each step. The two loops below visit the same cells, one with j on the inside and one with i. We print the first six addresses of a 4 by 4 matrix of float64 values.

main.pyPython
def row_major_trace(n):    return [(i * n + j) * 8 for i in range(n) for j in range(n)]def col_major_trace(n):    return [(i * n + j) * 8 for j in range(n) for i in range(n)]1print("row:", row_major_trace(4)[:6])print("col:", col_major_trace(4)[:6])
row: [0, 8, 16, 24, 32, 40]
col: [0, 32, 64, 96, 8, 40]
  1. 1With i inside, the next read is a whole row (here 4 values = 32 bytes) further.

Row by row the addresses go up by 8 bytes, the stride of the first topic, and a line serves eight reads. Column by column they jump by 32 bytes here, and by the length of a whole row in a bigger matrix. We now count the misses of the two orders, in a small 4 KB cache, for matrices from 16 by 16 up to 73 by 73.

main.pyPython
for n in (16, 32, 48, 73):    print(n, "x", n, "row:", count_misses(row_major_trace(n)), "col:", count_misses(col_major_trace(n)))
16 x 16 row: 32 col: 32
32 x 32 row: 128 col: 1024
48 x 48 row: 288 col: 2304
73 x 73 row: 667 col: 4573

A 16 by 16 matrix is 2 KB and fits in the cache, so after loading it both orders cost 32 misses, the same. From 32 by 32 the matrix no longer fits, and the column order misses on every single read: 1,024 misses against 128, a factor of eight, which is the eight values of a line. At 73 by 73 the factor is nearly seven. The shape of the loops did not change; the size of the data did.

Check the size, not only the shape

Swapping the loops is free when the matrix is bigger than the cache and does nothing when it fits. A test on a small matrix can hide the problem completely, and the program then slows down on real data. Measure at the real size.

02

When both loops disagree: the transpose

Sometimes no loop order helps. The transpose B[j][i] = A[i][j] reads A and writes B, and if the loops go along the rows of A they go down the columns of B. Whichever loop is inside, one of the two matrices is walked with a big stride. The cure is loop tiling: process the matrix in small square tiles, so that the few lines a tile touches are used many times before they are evicted.

  1. 1
    Choose a tile size tSmall enough that one tile of A and one of B fit in the cache.
  2. 2
    Loop over the tilesTwo outer loops with step t: ii and jj give the corner of a tile.
  3. 3
    Work inside the tileThe two inner loops visit only the t by t cells of this tile, as before.
  4. 4
    Move to the next tileOnly now does the program leave the lines of the old tile behind.

The code below only lists the order in which the cells are visited, for a 4 by 4 matrix, without and with tiles of 2. With tiles, the program finishes the top-left 2 by 2 block before it touches any other cell.

main.pyPython
def visit_order(n, tile=None):    step = n if tile is None else tile    order = []    for ii in range(0, n, step):1        for jj in range(0, n, step):            for i in range(ii, min(ii + step, n)):                for j in range(jj, min(jj + step, n)):                    order.append((i, j))    return orderprint("plain:", visit_order(4)[:8])print("tile 2:", visit_order(4, 2)[:8])
plain: [(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3)]
tile 2: [(0, 0), (0, 1), (1, 0), (1, 1), (0, 2), (0, 3), (1, 2), (1, 3)]
  1. 1The two outer loops walk over the tiles; the two inner loops walk inside one tile.

Now the real transposition. We write the trace of the reads of A and the writes of B, with B stored right after A, and count the misses for a 73 by 73 matrix with no tile and with tiles of 4, 8, 16 and 32.

main.pyPython
def transpose_trace(n, tile=None):    base_b = n * n * 8    step = n if tile is None else tile    trace = []    for ii in range(0, n, step):        for jj in range(0, n, step):            for i in range(ii, min(ii + step, n)):                for j in range(jj, min(jj + step, n)):                    trace.append((i * n + j) * 8)                    trace.append(base_b + (j * n + i) * 8)    return tracefor tile in (None, 4, 8, 16, 32):    print("tile", tile, "misses:", count_misses(transpose_trace(73, tile)))
tile None misses: 5996
tile 4 misses: 2603
tile 8 misses: 2036
tile 16 misses: 1960
tile 32 misses: 1714

Without tiling the transpose misses 5,996 times. With tiles of 4 it is already down to 2,603, and with tiles of 8 to 32 it stays between 2,036 and 1,714, about three times fewer. A tile that is too small wastes the edges of the lines it loads, so the gain grows with the tile until the tile stops fitting.

The best tile depends on the cache

A tile tuned for a 4 KB cache is wrong for a 32 KB one. Here everything from 8 to 32 is within 20% of the best, so a middling tile is a safe choice. When the machine changes, measure again instead of copying a number.

Libraries tile for you

NumPy and BLAS multiply and transpose big arrays in tiles already. You meet tiling when you write your own loops, in Numba, C++ or Rust, or when a custom kernel is slower than the library call it replaced.

What the real data says5,405 SPY closes as a 73 × 73 matrix (41 KB) in a 4 KB cache
Reading row by row667one miss per line of 8 values
Reading column by column4,5736.9 times more
Transpose, no tiling5,996reads and writes of both matrices
Transpose, tile of 321,7143.5 times fewer misses
Misses of the transpose by tile size
No tiling5996
Tile 42603
Tile 82036
Tile 161960
Tile 321714
03

What you will build

The lab follows the lecture: the two read orders, the transpose trace with an optional tile, the count of misses by order and by tile, the choice of the best tile and the study on the SPY matrix.

  • Function · questionBuilds on
  • row_major_trace(n)Which addresses does a row-by-row read touch?Plain arithmetic
  • col_major_trace(n)And a column-by-column read?Plain arithmetic
  • transpose_trace(n, tile=None)Which addresses does a transpose touch, tiled or not?Four nested loops
  • compare_orders(n)How much worse is the column order?row_major_tracecol_major_trace
  • misses_by_tile(n, tiles)How do tiles change the misses of the transpose?transpose_trace
  • best_tile(n, tiles)Which tile is best?misses_by_tile
  • study(bars)What does tiling gain on a matrix of SPY closes?compare_ordersbest_tile

Key takeaways

  • Put the loop that walks the storage inside. For a row-major matrix the inner loop should change j; the column order misses on every read once the matrix outgrows the cache.
  • Tile when the loops disagree. A transpose cannot please both matrices; tiles of 8 to 32 cut its misses about three times.
  • Measure at the real size. A matrix that fits in the cache hides all of this, and the best tile changes with the cache.
Quick check
Which statement is true for reading a matrix column by column instead of row by row?
Pick an answer.

Next topic: tiling hides the delay of a miss by reusing lines; prefetching hides it by asking for a line before it is needed. In Prefetching you add a stride detector to the cache and see which access patterns it can predict, and which, like a random walk through a table, it cannot.

Lab · what's graded

7 tasks · 100 pts
  • Reading row by row10 pts
  • Reading column by column10 pts
  • The transpose, tiled or not25 pts
  • Comparing the orders15 pts
  • The misses by tile15 pts
  • The best tile10 pts
  • The study on the SPY matrix15 pts
Explorer · lab-cl-6
▾ lab-cl-6
▾ data
…
bash · in-browser
learner@quantcraft:~/lab-cl-6$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-cl-6 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes