Midterm Cheat Sheet
Work / Optimization Principles
- Work: total number of operations executed.
- First reduce work, then reduce cycles per unit work.
- On a single core, parallelism / work-stealing do not help.
- Helpful single-core optimizations:
- loop unrolling
- hoisting
- short-circuiting
- common-subexpression elimination
- vectorization
- inlining
- Inlining removes call overhead but can hurt i-cache locality.
Bit Hacks
Core identities
\[ x + \sim x = -1 \] \[ -x = \sim x + 1 \]Single-bit mask with \(m = (1 \ll k)\):
- set bit:
x |= m - clear bit:
x &= m - toggle bit:
x ^= m
Core idioms:
- least-significant 1 bit:
x & (-x) - clear least-significant 1 bit:
x & (x-1) - power of 2:
x > 0 && (x & (x-1)) == 0 - circular buffer of size \(2^k\):
x = (x+1) & ((1<<k)-1) - branchless minimum:
min(x,y) = y ^ ((x ^ y) & -(x < y)) - branchless maximum:
max(x,y) = x ^ ((x ^ y) & -(x < y))
No-temp XOR swap:
x ^= y;
y ^= x;
x ^= y;
Builtins to know:
__builtin_ctzll(x)__builtin_clzll(x)__builtin_popcountll(x)__builtin_assume_aligned(ptr, align)
Hailstone Optimization
Instead of repeatedly dividing by 2 in a loop, compress all trailing factors of 2 at once:
int z = __builtin_ctzll((unsigned long long)n);
n >>= z;
iterations += z;
Why faster:
- fewer instructions
- fewer unpredictable branches
N-Queens Bitmask Recurrence
State:
down: occupied columnsleft: blocked by left diagonalsright: blocked by right diagonalsmask: low \(n\) bits set
Core recurrence:
possible = (down | left | right) & mask;
place = possible & (-possible);
possible &= place;
Updates:
down' = down | place;
left' = ((left | place) << 1) & mask;
right' = (right | place) >> 1;
Base case:
down == mask
Locality / Caches / Memory Hierarchy
- Spatial locality: nearby addresses likely reused soon.
- Temporal locality: same address likely reused soon.
- Cache moves data in cache lines, not individual words.
- Unit-stride access is much better than strided / irregular access.
- Loop order matters for locality.
Cache miss types:
- Cold miss: first access to a block
- Capacity miss: working set too large for cache
- Conflict miss: mapping collisions in set-associative / direct-mapped cache
- True sharing: cores communicate via same cache line
- False sharing: unrelated variables share one line, causing coherence traffic
MSI / Cache Coherence
- M: modified, exclusive dirty copy
- S: shared, clean copy
- I: invalid
- Before writing a shared line, hardware must invalidate other copies.
- Shared \(\to\) Invalid transition is possible.
- Set-associative caches reduce conflict misses relative to direct-mapped caches.
x86-64 Essentials
- Course uses AT&T syntax: destination is the last operand.
- Suffixes:
\[ \texttt{b}=8\text{-bit},\quad \texttt{w}=16\text{-bit},\quad \texttt{l}=32\text{-bit},\quad \texttt{q}=64\text{-bit} \]
cmp/testset flags,jccreads them.- Important flags: ZF, SF, CF, OF.
- Addressing mode:
\[ \texttt{disp(base,index,scale)} = \texttt{base + index*scale + disp} \]
leacomputes addresses without dereferencing memory.
Calling Convention / Stack
Integer / pointer args:
\[ \%rdi,\ \%rsi,\ \%rdx,\ \%rcx,\ \%r8,\ \%r9 \]Floating-point args:
\[ \%xmm0 \text{ to } \%xmm7 \]Callee-saved:
\[ \%rbx,\ \%rbp,\ \%r12,\ \%r13,\ \%r14,\ \%r15 \]Caller-saved: everything else.
Standard prologue:
pushq %rbp
movq %rsp, %rbp
- stack grows downward
callpushes return addressretpops return address
LLVM IR Essentials
- SSA: each register written at most once.
icmp: comparebr: conditional / unconditional branchphi: select value based on predecessor blockgetelementptr: address computation
phi is not a real machine instruction.Autovectorization
For AVX2:
- 256-bit vector registers
- FP32: 8 lanes
- FP64: 4 lanes
Why compilers fail to vectorize:
- unknown trip count
- cannot prove independence / aliasing
- loop-carried dependence
- reductions
- control flow
- non-contiguous memory / gather
- cost model says not profitable
Important facts:
- floating-point addition is not associative
restricthelps prove no aliasing-ffast-mathmay be needed for floating-point vectorization
Multicore / Cilk / Reducers
cilk_spawnandcilk_syncgrant permission for parallel execution.- They do not force parallel execution.
cilk_forrequires independent iterations.
Serial semantics:
#define cilk_for for
#define cilk_spawn
#define cilk_scope
Determinacy race: two logically parallel instructions access the same location and at least one writes.
Bad example:
int answer = A[0];
cilk_for (int i = 0; i < n; i++) {
answer = min(answer, A[i]);
}
Use a reducer instead.
Parallelism / Work / Span
\[ S_P = \frac{T_1}{T_P} \] \[ T_P \ge \frac{T_1}{P} \qquad\text{(Work Law)} \] \[ T_P \ge T_\infty \qquad\text{(Span Law)} \] \[ \text{Parallelism} = \frac{T_1}{T_\infty} \]Amdahl's Law:
\[ S_P \le \frac{1}{\alpha + (1-\alpha)/P} \]Greedy scheduling theorem:
\[ T_P \le \frac{T_1}{P} + T_\infty \]Near-linear speedup when:
\[ \frac{T_1}{T_\infty} \gg P \]Parallel slackness:
\[ \frac{T_1/T_\infty}{P} \]Parallel Loop Formulas
Nested independent \(n \times n\) parallel loops:
\[ T_1(n) = \Theta(n^2) \] \[ T_\infty(n) = \Theta(\log n) \] \[ \text{Parallelism} = \Theta\!\left(\frac{n^2}{\log n}\right) \]Coarsened loop with grainsize \(G\), iteration cost \(I\), and scheduling overhead \(S\):
\[ T_1(n) = nI + (n/G - 1)S \] \[ T_\infty(n) = GI + \log(n/G)\,S \]Want:
\[ G \gg S/I \]but not so large that parallelism collapses.
Alternative coarse-grained span model:
\[ T_\infty(n) = \Theta(G + n/G) \]Best balance:
\[ G = \sqrt{n} \]Master Method / Common Recurrences
\[ T(n) = aT(n/b) + f(n) \]Useful ones:
\[ 2T(n/2) + \Theta(n) = \Theta(n \log n) \] \[ 3T(n/2) + \Theta(n) = \Theta(n^{\lg 3}) \] \[ 8T(n/2) + \Theta(n^2) = \Theta(n^3) \] \[ T_\infty(n) = T_\infty(n/2) + \Theta(\log n) = \Theta(\log^2 n) \] \[ T_\infty(n) = 2T_\infty(n/2) + \Theta(1) = \Theta(n) \]Measurement / Timing
- Best raw statistic for runtime: minimum
- For multiplicative benchmark ratios, use geometric mean
- Major noise sources:
- other processes
- interrupts
- DVFS / Turbo Boost
- hyperthreading
- cache / page alignment
Tools:
/usr/bin/time: whole-program timingclock_gettime(CLOCK_MONOTONIC): region timingrdtsc(): cycle count, but tricky across cores / DVFSperf stat: hardware counters
Quiescing tips:
- run on quiet system
- pin with
taskset - avoid core 0 for serial benchmarks
- warm cache vs cold cache matters
Virtual Memory / TLB / Allocators
- page size usually \(4\text{ KiB}\)
- page faults are extremely expensive
- TLB caches virtual-to-physical translations
- TLB miss does not imply cache miss
- L1 lookup can overlap with translation using page-offset bits
Allocator metrics:
\[ F = \frac{H}{M} \qquad\text{fragmentation} \] \[ U = \frac{M}{H} \qquad\text{space utilization} \]where
- \(M\): user footprint
- \(H\): allocator footprint
Fragmentation types:
- internal fragmentation: block larger than requested
- external fragmentation: enough total free space exists, but not contiguously
NUMA / first-touch:
- page goes to the socket of the first thread that touches it
- pinning threads alone does not control memory placement
Memory-Level Parallelism
Little's Law:
\[ L = \lambda W \]Interpretation for memory:
- \(W\): latency
- \(\lambda\): throughput
- \(L\): memory-level parallelism
- pointer chasing often gives MLP \(= 1\)
- independent address generation allows multiple outstanding misses
- hardware prefetchers help for regular patterns
- software prefetch can help when future accesses are predictable enough