A one-page reference for loop reordering, fusion/fission, cache blocking, SIMD/auto-vectorization,
do concurrent, contiguous, and the judgment to stop — the chapter that turns measurement into speed.
The optimization ladder (largest payoff first)
Rung
Technique
When it pays
Readability cost
1
-O2/-O3+ correct loop order
always (dense Fortran)
none
2
Loop fusion + hoist invariant divisions
two passes share data
negative (shorter!)
3
Vectorization (do concurrent, contiguous, -march=native)
clean unit-stride inner loops
low
4
Cache blocking
compute-bound, high-reuse kernels only
medium
5
Hand-tuning (intrinsics, manual unroll)
rarely — profiler must prove it
high
—
Call a tuned BLAS
any dense linear algebra
none — always wins
Loop reordering — the one rule with no exceptions
$$
\text{inner loop over the FIRST index} \;\Longrightarrow\; \text{unit stride, full cache lines}
$$
Fortran is column-major: a(i,j) and a(i+1,j) are adjacent in memory; a(i,j) and a(i,j+1) are a
whole column apart.
Inner loop over i (first index) = fast; over j (second index) = several × slower on a large array — for
the identical result.
NumPy is row-major (opposite): its fast inner axis is the last one. Mind this at the f2py boundary.
do j = 1, n
do i = 1, n ! i inner: consecutive iterations touch consecutive memory
a(i,j) = ...
end do
end do
Fusion, fission, hoisting
Transform
Does
Use when
Fusion (jamming)
merges 2 loops → 1; touches each element once; kills a temporary
two loops share data you move twice
Fission (distribution)
splits 1 loop → 2; isolates a vectorizable part
a call/branch blocks vectorizing a clean part
Hoisting
move a loop-invariant out of the loop
a divide (costly) inside an inner loop
Hoist divides (many × a multiply's cost); doing rx = alpha*dt/dx**2 once beats $n^2$ divides per step.
Cache blocking (tiling) — and when NOT to
Blocking works a small tile that stays cache-resident through all its reuses, then advances.
Helps compute-bound, high-reuse kernels (matrix multiply: $O(n^3)$ work / $O(n^2)$ data → $O(n)$ reuse).
Does almost nothing for a memory-bound single stencil sweep (low reuse). Loop order + fusion are its
whole story.
Decide by arithmetic intensity (flops per byte) and the roofline:
Kind
Arithmetic intensity
Ceiling
Optimize by
Memory-bound (stencil, ~0.1 flop/byte)
low
memory bandwidth
move less data (loop order, fusion)
Compute-bound (matmul)
high
arithmetic rate
keep units fed (blocking, vectorize, BLAS)
SIMD and auto-vectorization
SIMD = one instruction, many operands: AVX register = 4real(dp); AVX-512 = 8.
Auto-vectorization = compiler emits SIMD from a plain loop (no intrinsics), at -O2+.
A loop vectorizes when it is: independent (no loop-carried dependency), unit-stride, countable,
non-aliasing, simple body (no un-inlinable calls).
y(i) = a*x(i) + y(i) ! vectorizes (independent)
y(i) = y(i-1) + x(i) ! does NOT (loop-carried dependency)
do concurrent (j = 2:ny-1, i = 2:nx-1) ! "these iterations are independent"
u_new(i,j) = ... ! reads OLD u, writes NEW u_new
end do
A promise the compiler trusts, not a parallel directive. It permits vectorization/parallelization; it
does not guarantee parallel execution (needs -ftree-parallelize-loops, or nvfortran for GPU).
Break the promise (in-place update, a running total) → undefined result. Keep read/write arrays distinct.
real(dp), intent(in), contiguous :: u(:,:) promises unit stride — the compiler skips a contiguity
check / copy-in-out and may vectorize. Most valuable on pointers (which may point at strided slices).
Fortran assumes procedure arguments do not alias (the Ch. 27 advantage) — that is why the stencil
vectorizes. Pointers/targets can alias and forfeit it; prefer allocatable, keep read/write arrays
distinct, mark pointers contiguous.
Verifying an optimization (correctness is paramount)
enable (more) optimization, including auto-vectorization
-march=native
use the widest SIMD (AVX/AVX-512) your CPU has
-fopt-info-vec / -fopt-info-vec-missed
report which loops vectorized (and why not)
Pitfalls
Inner loop over the second index — strided, column-major-hostile; several × slow. The #1 numerical-Fortran bug.
A wasted pass / temporary — a full array written and re-read is pure traffic for a memory-bound kernel.
do concurrent with a hidden dependency — undefined result; the compiler trusts, does not check.
Plain pointer in a hot loop — assumed non-contiguous and possibly aliasing → no vectorization; add contiguous.
Blocking a memory-bound sweep / hand-writing intrinsics — effort for near-zero gain; wrong rung of the ladder.
Optimizing before measuring — the profiled 3% is where effort belongs; the other 97% stays readable.
Numbers / rules worth memorizing
Inner loop over the first index. The single rule with no exceptions.
Most stencils are memory-bound: optimize by moving less data; you cannot out-compute the memory ceiling.
A tuned dgemm beats a hand-rolled matrix multiply by ~an order of magnitude — call the library.
Optimize in payoff order; the first hour buys most of the speedup (Knuth's "critical 3%").
The heat-solver piece added
The stencil step is now optimized: correct loop order, one fused sweep (no lap temporary, divisions
hoisted), two buffers swapped via contiguous pointers (O(1), no copy), expressed as do concurrent —
fast, bit-for-bit identical to Chapter 24, and ready to go parallel behind the same interface. Next,
Chapter 30 picks the flags that finish the job.