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})}. \]
IMPORTANT: Pointer chasing is latency-bound. Independent addresses and prefetching hide latency.

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.
\[ Q_{\mathrm{LRU},2\mathcal M}\le 2Q_{\mathrm{OPT},\mathcal M}. \]

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). \]
IMPORTANT: Tiling needs cache-size parameter; cache-oblivious recursion gets same asymptotic without tuning.

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.

\[ \text{one lock}\Rightarrow\text{serialization + cache-line bouncing}. \] \[ \text{many locks}\Rightarrow \text{throughput}\approx \min(\#\text{locks},\#\text{workers}) \]

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}. \]
IMPORTANT: Locks remove some races but introduce nondeterministic ordering and bottlenecks.

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_lock to 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.

IMPORTANT: Proofs assuming SC may fail on real hardware. Use atomics/fences.

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:

\[ \text{deadlock freedom can hold, but mutual exclusion can fail}. \]

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.

IMPORTANT: Alpha-beta does not miss the best move; it prunes only after proving a branch irrelevant.

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}). \]
IMPORTANT: CSR/CSC are traversal-friendly but insertion-hostile.

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}. \]
IMPORTANT: Compilers are good at local mechanical rewrites, not algorithmic redesign.

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:

\[ \texttt{wants=true; turn=other; fence; while(other\_wants && turn==other);} \]

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.