Quiz 2 Cheat Sheet
Memory-Level Parallelism / Bandwidth
MLP: number of outstanding memory requests.
\[ L=\lambda W,\qquad L=\text{MLP},\quad W=\text{latency},\quad \lambda=\text{throughput},\quad \text{time/access}\approx W/L. \] \[ \text{BW/worker}\approx \frac{(\#\text{outstanding lines})(\text{line bytes})}{\text{latency}}, \qquad \text{workers to saturate}= \frac{\text{machine BW}}{\text{BW/worker}}. \]Patterns:
n=A[n]: pointer chasing; next address depends on loaded value; MLP \(\approx 1\).n=2*n mod p: address independent of loaded value; OoO can issue many misses.- sequential/stride: stream/stride prefetchers help.
- random access: prefetchers mostly fail.
- L1 LFBs handle demand misses; L2 stream prefetcher can expose more MLP.
- MLP enablers: out-of-order execution, hardware prefetching, branch prediction.
Amdahl with memory cap:
\[ S\le \frac{1}{f+(1-f)/\min(P,\text{parallelism},\text{BW-saturating workers})}. \]Cache Model / Address Bits
Ideal cache: size \(\mathcal M\), line \(\mathcal B\), fully associative, optimal/LRU replacement. For real cache size \(C\), associativity \(A\), line size \(B\), address width \(w\):
\[ \#\text{sets}=\frac{C}{AB},\qquad \text{offset bits}=\lg B,\qquad \text{set bits}=\lg(\#\text{sets}), \] \[ \text{tag bits}=w-\text{set bits}-\text{offset bits}. \]Miss types:
- Cold: first access to block.
- Capacity: working set too large.
- Conflict: set collision.
- True sharing: same data.
- False sharing: different data same line.
Segment lemma: contiguous segments total \(N<\mathcal M/3\), average segment size \(\ge\mathcal B\):
\[ Q=O(N/\mathcal B). \]Tall cache:
\[ \mathcal B^2< c\mathcal M. \]Submatrix lemma: \(n\times n\) row-major submatrix fitting tall cache:
\[ Q=O(n^2/\mathcal B). \]If whole working set fits:
\[ Q=\Theta(\text{working set size}/\mathcal B). \]Cache-Efficient Algorithms
Naive matrix multiply:
\[ T(n)=\Theta(n^3). \]Bad row-major access \(B[k][j]\):
\[ Q(n)=\Theta(n^3) \quad \text{if } n\ge \mathcal M/\mathcal B, \] \[ Q(n)=\Theta(n^3/\mathcal B) \quad \text{if } \sqrt{\mathcal M}\le n<\mathcal M/\mathcal B, \] \[ Q(n)=\Theta(n^2/\mathcal B) \quad \text{if } n< c\sqrt{\mathcal M}. \]Tiled matrix multiply:
\[ s=\Theta(\sqrt{\mathcal M}),\qquad Q(n)=\Theta\left(\frac{n^3}{\mathcal B\sqrt{\mathcal M}}\right). \]Cache-oblivious D&C matrix multiply:
\[ Q(n)= \begin{cases} \Theta(n^2/\mathcal B), & n^2< c\mathcal M,\\ 8Q(n/2)+O(1), & \text{otherwise}. \end{cases} \] \[ Q(n)=\Theta\left(\frac{n^3}{\mathcal B\sqrt{\mathcal M}}\right). \]Generic cache-recursive rectangle:
\[ Q(r,w)\le \begin{cases} 2Q(r/2,w)+O(1), & r\ge w,\ r+w> \alpha\mathcal M,\\ 2Q(r,w/2)+O(1), & r< w,\ r+w> \alpha\mathcal M,\\ \Theta(\mathcal M/\mathcal B), & r+w\le\alpha\mathcal M. \end{cases} \] \[ W(r,w)=\Theta(rw),\qquad h=\lg(rw)-2\lg\mathcal M, \] \[ \#\text{leaves}=\Theta(rw/\mathcal M^2),\qquad \text{misses/leaf}=\Theta(\mathcal M/\mathcal B), \] \[ Q=O\left(\frac{rw}{\mathcal M\mathcal B}\right). \]Loop rule: if inner array is too large and reloaded every outer iteration:
\[ Q=\Theta(\#\text{outer}\cdot \#\text{inner}/\mathcal B). \]Cache-Oblivious Stencils
Heat update:
\[ u[t+1][x]=u[t][x]+\alpha(u[t][x+1]-2u[t][x]+u[t][x-1]). \]Even-odd trick: keep only \(u[t\bmod 2][x]\). Looping 1D stencil with \(N>\mathcal M\):
\[ Q=\Theta(NT/\mathcal B). \]Naive 2D stencil with \(n^2>\mathcal M\):
\[ Q=\Theta(tn^2/\mathcal B). \]Trapezoid recursion:
- if width \(\ge 2h\), space cut
- if width \(<2h\), time cut
- base case: \(h=1\)
If leaf width \(w=\Theta(\mathcal M)\):
\[ \#\text{leaves}=\Theta(NT/w^2), \qquad \text{misses/leaf}=\Theta(w/\mathcal B), \qquad Q=\Theta\!\left(\frac{NT}{\mathcal B\mathcal M}\right). \]Parallel stencil: time cuts cannot parallelize. Space cuts can parallelize.
Determinism / Races / Locks
Deterministic: every memory location sees same sequence of updates every execution.
\[ \text{No Cilk determinacy races}\Rightarrow\text{deterministic}. \]Determinacy race: logically parallel instructions access same location and at least one writes.
\[ \text{read race}=\text{read/write},\qquad \text{write race}=\text{write/write}. \]Data race: logically parallel strands hold no common lock, access same location, and at least one writes.
\[ \text{no data races}\not\Rightarrow\text{no bugs}. \]Atomic = system never observes sequence partially executed. Critical section = shared-data code requiring mutual exclusion.
Locks:
- spinning burns cycles
- yielding gives CPU to OS
- reentrant lets owner relock
- nonreentrant owner relock deadlocks
- fair = FIFO-ish
- unfair = any waiter can acquire
Contention: repeated xchg invalidates everyone else's line. Test-and-spin reads until free, then atomically exchanges. Backoff waits after failure.
until contention or bandwidth dominates. Performance ranking:
\[ \text{fits L1 + no contention}> \text{fits L2/L3 + no contention}> \text{low contention parallel}> \text{high contention parallel}. \]Deadlock / Convoys
Deadlock needs:
\[ \text{mutual exclusion}+\text{nonpreemption}+\text{circular wait}. \]Global lock order:
\[ L_1\prec L_2\prec\cdots\prec L_n,\qquad \text{all threads acquire increasing order} \Rightarrow \text{no deadlock}. \]Valid all-lock acquisition rules must impose one global order over all locks. Random start/order can deadlock. Rules:
- do not hold mutex across sync/join
- keep locks inside
cilk_scope - use
try_lockto avoid convoys
Lock convoy: many threads repeatedly block on same lock.
Allocators / Garbage Collection
Bitmap allocator:
- good for tiny objects: less internal fragmentation
- compact metadata can reduce pages/TLB misses
- may have higher allocation latency due to scanning
Free list:
- fast allocation/free via pointer pop/push
- pointer/index metadata hurts tiny objects
Local ownership allocator:
- each block marked with owner thread
- owner eventually frees block
- can be resilient to false sharing
- remote frees can require synchronization
- can suffer blowup/drift under imbalance
Stop-and-copy GC:
- compacts live objects
- improves external fragmentation
- must update pointers
- not necessarily asymptotically faster
Mark-and-sweep: does not move objects; can leave external fragmentation.
Memory Models / TSO
Sequential consistency: all operations appear in one global total order preserving each processor's program order; load reads latest previous store to that address.
Store-buffering:
\[ P_0:\ a=1;\ r_0=b, \qquad P_1:\ b=1;\ r_1=a. \]Under SC:
\[ r_0=r_1=0\quad\text{impossible}. \]x86-64 TSO:
- loads not reordered with loads
- stores not reordered with stores
- stores not reordered with prior loads
- load may bypass prior store to different address
- load cannot bypass prior store to same address
- locked instructions globally ordered
Store buffer: stores wait before globally visible; later loads to other addresses can bypass. Fence prevents later memory ops from bypassing earlier ones. Loads check own store buffer for same address.
Peterson / Atomics / CAS
Peterson:
A_wants = true;
turn = B;
fence;
while (B_wants && turn == B);
critical_section();
A_wants = false;
Under SC: mutual exclusion, deadlock freedom, starvation freedom. Proof idea: if both enter, WLOG Bob wrote turn last; Bob sees A_wants=true and turn=A, so Bob spins.
On TSO, fence must be after writing turn, before the while loop. If fence moved before turn:
Fences can also act as compiler barriers. Volatile alone is not a full synchronization primitive.
Use _Atomic, atomic_load, atomic_store, atomic_thread_fence().
CAS: atomically replace if memory still equals expected value.
\[ \texttt{CAS(addr, old, new)}. \]CAS update:
do {
old = *addr;
new = f(old);
} while (!CAS(addr, old, new));
Lock-free push:
do {
old = head;
node->next = old;
} while (!CAS(&head, old, node));
ABA: head changes \(A\to B\to A\), CAS succeeds although structure changed. Fix: version/tag pointer or delayed reclamation/hazard pointers.
Speculative Parallelism / Alpha-Beta
Speculative parallelism: spawn work serial projection may never do; trades extra work for parallelism.
Game tree:
- MAX takes max child
- MIN takes min child
- leaf uses static eval
Full depth-\(d\) tree:
\[ 1+b+\cdots+b^d=\Theta(b^d). \]Alpha-beta:
search(x, alpha, beta, d):
if d==0 or leaf: return eval(x)
for child c:
s = -search(c, -beta, -alpha, d-1)
alpha = max(alpha, s)
if alpha >= beta: return alpha
return alpha
\[
\alpha=\text{best current player can force},
\qquad
\beta=\text{opponent cutoff threshold}.
\]
If \(\alpha\ge\beta\), opponent avoids this line.
Best-ordered alpha-beta:
\[ \Theta(b^{d/2}) \]nodes; square-roots work/branching factor and roughly doubles depth.
Changes searched nodes:
- move ordering: yes
- leaf eval: yes
- transposition table: yes
- board representation: usually no
-O3: usually no
Young Brothers Wait: search first child serially; if no cutoff, search rest in parallel.
\[ T_1=\Theta(b^{d/2}), \qquad T_\infty=\Theta(2^{d/2})\quad\text{one-child wait}, \] \[ T_\infty=\Theta(3^{d/2})\quad\text{two-child wait}. \]Zero-window/scout: checks above/below threshold; returns bound, not exact score; full-window needed if move may improve best.
Sparse / Structured Data
Arrays are fast: contiguous, no pointer chasing, cache/prefetch/vector friendly. Dense row-major:
\[ \text{locate}(i,j)=i\cdot n+j. \]Formats:
- CSR:
pos=row starts,crd=column indices,vals=values. - CSC:
pos=column starts,crd=row indices,vals=values. - COO: rows, columns, values.
- RLE: runs of repeated values; row-major vs column-major matters.
Recognition:
\[ \text{COO}=(\text{rows},\text{cols},\text{vals}), \quad \text{CSR}=(\text{rowptr},\text{cols},\text{vals}), \] \[ \text{CSC}=(\text{colptr},\text{rows},\text{vals}), \quad \text{RLE}=(\text{value},\text{run length}). \]Sparse iteration:
- dense-sparse: sparse leads, dense follows with \(O(1)\) lookup.
- sparse multiplication \(a_i=b_i c_i\): coordinate intersection.
- sparse addition \(a_i=b_i+c_i\): coordinate union.
Two-finger merge:
while (i < nnzA && j < nnzB) {
a = A_crd[i];
b = B_crd[j];
m = min(a,b);
if (a == m && b == m) compute;
if (a == m) i++;
if (b == m) j++;
}
SpMV:
\[ \Theta(\mathrm{NNZ})\quad\text{instead of}\quad \Theta(NM). \]SpMM: row work imbalanced; block compressed storage, not coordinates.
SDDMM / compound sparse expression:
\[ A_{ij}=B_{ij}\cdot f(C,D). \]If \(B_{ij}=0\), skip; fuse kernels; do not materialize dense intermediate.
Graphs
Graph storage: adjacency matrix simple/dense; adjacency list or CSR sparse/traversal-friendly.
Parallel BFS push:
- process frontier in parallel
- CAS
depth[u]from \(-1\) to new depth - successful CAS discovers vertex
Pull BFS: unvisited vertices check whether any incoming neighbor is in frontier; better when frontier large.
\[ \text{small frontier}\Rightarrow\text{push}, \qquad \text{large frontier}\Rightarrow\text{pull}. \]PageRank locality:
- push: random writes/atomics into
new_ranks - pull: reads neighbors, avoids atomics
- partition/reorder graph; BCSR improves locality
GPUs
Warp \(=32\) threads executing together. Block = one or more warps. Grid = one or more blocks. Bad patterns:
\[ \text{noncoalesced global memory accesses}, \qquad \text{divergent branches within same warp}. \]Less bad:
\[ \text{different warps taking different branches}. \]Coalescing matters most for global DRAM accesses. Hardware tradeoff:
\[ \text{amortize control overhead with wide warps} \quad\text{vs}\quad \text{wasted work from diverging control flow}. \]Compiler Optimizations
Pipeline:
\[ \text{C/C++}\to\text{LLVM IR}\to\text{optimized LLVM IR}\to\text{assembly}. \]Passes run in heuristic order, may run multiple times, one pass enables another, order not globally optimal.
Optimizations:
- constant folding/propagation: compute/substitute constants
- CSE: reuse repeated expression
- inlining: replace call with body
- hoisting: move loop-invariant work out
- loop unswitching: move loop-invariant branch out
- combining tests: selects / lookup tables
- strength reduction: shifts, LEA, magic multiply
- Mem2Reg: stack locals \(\to\) SSA registers
- SROA: struct/aggregate \(\to\) scalar fields
At -O0: lots of alloca, store, load; locals live in stack slots.
Compiler likely:
\[ (a.x-b.x)(a.x-b.x) \quad\Rightarrow\quad \text{CSE}. \]If constants are hard-coded and function is inlined:
\[ \text{inlining}+\text{constant folding}+\text{CSE}. \]Compiler limitations:
- must be correct for all legal inputs
- limited hot-path/input knowledge
- imperfect hardware cost model
- analyses may be hard/undecidable
- usually cannot invent algorithmic symmetry
Example compiler likely cannot discover:
\[ F_{12}=-F_{21}. \]Quiz 2: Big Things To Know First
1. Identify the bottleneck
Ask: is this limited by work, span, cache misses, bandwidth, memory latency, lock contention, or nondeterminism?
\[ T_P\ge T_1/P,\qquad T_P\ge T_\infty,\qquad \text{parallelism}=T_1/T_\infty. \]If memory-bound:
\[ L=\lambda W,\qquad \text{throughput}\approx L/W,\qquad \text{workers to saturate BW}=\frac{\text{machine BW}}{\text{BW per worker}}. \]Pointer chasing gives MLP \(\approx 1\). Independent addresses, OoO, branch prediction, and prefetching expose MLP.
2. Cache-analysis recipe
If whole working set fits:
\[ Q=\Theta(\text{working set}/\mathcal B). \]If inner data is too large and reloaded each outer iteration:
\[ Q=\Theta(\#\text{outer}\cdot \#\text{inner}/\mathcal B). \]For recursive cache-oblivious algorithms:
\[ \#\text{leaves}\times \text{misses per leaf}. \]Use tall-cache/submatrix lemmas when row-major submatrices fit.
3. Races and determinism
\[ \text{determinacy race}=\text{parallel same-location access with at least one write}. \] \[ \text{data race}=\text{parallel same-location access with at least one write and no common lock}. \]No determinacy races \(\Rightarrow\) deterministic. Locks remove some data races but introduce nondeterministic order, contention, convoys, and deadlock risk.
4. Locking / deadlock / contention
Deadlock needs mutual exclusion, nonpreemption, and circular wait.
\[ \text{global lock order}\Rightarrow\text{no circular wait}\Rightarrow\text{no deadlock}. \]One hot lock serializes. With \(k\) locks:
\[ \text{effective parallelism}\approx \min(k,P) \]until contention/cache-line bouncing/BW dominates.
5. Memory model / Peterson
SC: one global total order preserving each thread's program order. TSO: stores sit in store buffers; later loads to different addresses may bypass earlier stores. Fence after writing turn is the important Peterson placement:
Volatile is not a full synchronization primitive; atomics/fences are the real tool.
6. Allocators / GC
Bitmap: tiny objects, low internal fragmentation, compact metadata, maybe slower scan. Free list: fast pop/push, but pointer/index metadata hurts tiny objects. Global heap: no drift but lock contention. Local ownership: fast local alloc, resilient to false sharing, but remote frees/synchronization and drift/blowup possible. Mark-sweep: does not move objects, can leave external fragmentation. Stop-copy: moves/copies live objects, compacts, updates pointers, improves external fragmentation.
7. Alpha-beta / speculation
Full game tree:
\[ 1+b+\cdots+b^d=\Theta(b^d). \]Best-ordered alpha-beta:
\[ \Theta(b^{d/2}) \]nodes. It does not miss the best move; it prunes only when a branch is proven irrelevant. Speculative parallelism creates work the serial projection might not do.
8. Sparse / structure
Dense arrays are fast because contiguous. Sparse saves work only if structure is used.
\[ \text{CSR}=(\text{rowptr},\text{cols},\text{vals}),\quad \text{CSC}=(\text{colptr},\text{rows},\text{vals}),\quad \text{COO}=(\text{rows},\text{cols},\text{vals}). \]Multiply sparse coordinates by intersection; add by union. RLE is for repeated runs.