Parallel Algorithms & Design
Complete revision notes. Everything in this unit, organised by topic — definitions, diagrams, formulas, comparison tables and worked examples. Revise from this alone and you have the whole unit.
Contents
A · Designing algorithms
B · Measuring performance
Quick recall — every table and formula in one place · Self-check questions
Part A — Designing Parallel Algorithms
A1. Design, don't retrofit
A parallel algorithm is not a serial algorithm with pragmas added. Sprinkling #pragma omp parallel for over existing code usually produces either an incorrect program or a slower one, because the structure of the computation determines how much parallelism is available. Unit III is about deciding that structure deliberately, and about measuring whether the decision worked.
A2. Foster's PCAM methodology
Ian Foster's four-stage design methodology is the standard framework for designing a parallel algorithm.
| Stage | The question it answers | |
|---|---|---|
| P | Partition | Break the problem into the smallest independent tasks. Think small, think independent, ignore the hardware. |
| C | Communication | Which tasks need data from which other tasks? Identify the dependencies. |
| A | Agglomeration | Group tasks together to reduce communication and scheduling overhead. |
| M | Mapping | Which processor or core runs each group of tasks? |
P + C ---- EXPOSE parallelism ---- ignore the hardware,
aim for ~10x more tasks
than processors
A + M ---- MAKE IT PRACTICAL ---- group and place that work
so it runs fast on real
hardware
The governing principle is expand, then contract. First find the maximum possible parallelism without regard to the machine; only then coarsen it to fit the machine you actually have. Designing in that order rarely goes wrong; designing in the reverse order — starting from "I have 8 cores" — hides parallelism you will never recover.
A3. Decomposition techniques
| Technique | How the problem is split | Examples | Scaling |
|---|---|---|---|
| Data / domain | Same operation applied to different slices of the data | Sum an array; resize 1000 images; stencil on a grid | Excellent — scales with data size. GPUs live here. |
| Functional / task | Different operations, often forming a pipeline | Video: decode → filter → encode | Limited by how many distinct stages exist |
| Recursive | Divide and conquer into same-shaped sub-problems | Quicksort, merge sort, tree reduction | Naturally spawns independent sub-tasks |
| Exploratory | Search a space; the first worker to find a solution stops the rest | Game-tree search, constraint solving, brute-force search | Finish time depends on luck; needs a stop signal |
| Speculative | Start work that may not be needed; discard the waste | Branch prediction, speculative execution of a transaction | Trades wasted work for reduced latency |
Real systems mix several of these. Data decomposition is by far the most common and the most scalable, because the amount of data typically grows without limit while the number of distinct functional stages in a program does not.
A4. Granularity
Granularity is the size of a task, usually expressed as the computation-to-communication ratio — the amount of useful work done per unit of coordination.
| Fine-grained | Coarse-grained | |
|---|---|---|
| Description | Many small tasks | Few large tasks |
| Advantage | Excellent load balance | Low overhead |
| Disadvantage | High scheduling and communication overhead | Risk of load imbalance; idle cores |
| Failure mode | Too fine → you parallelise yourself slower than serial | Too coarse → seven cores wait for the eighth |
Worked example — how big should a task be? 1,000,000 items, each requiring 1 µs of real work (so 1 second serially). Creating and scheduling one task costs 10 µs. There are 8 cores. The only decision is how many items go into one task.
| Items per task | Tasks | Total overhead | Wall-clock on 8 cores | Speedup |
|---|---|---|---|---|
| 1 (too fine) | 1,000,000 | 10 s | 1.375 s | 0.7× — slower than serial |
| 100 | 10,000 | 0.10 s | 0.138 s | 7.3× |
| 1,000 (sweet spot) | 1,000 | 0.01 s | 0.126 s | 7.9× |
| 125,000 | 8 (one each) | 0.00008 s | 0.125 s | 8.0× — but no slack for imbalance |
| 1,000,000 (too coarse) | 1 | 0.00001 s | 1.0 s | 1.0× — seven cores idle |
speedup
8x | ____________
| / \
| / \ <-- you fall off BOTH edges
| / \
1x |_______/ \____
+-------------------------------------
1 100 1,000 125,000 1,000,000
items per task
Note the last row of the table carefully: exactly one task per core gives the best arithmetic but leaves no slack, so any variation in task cost immediately becomes idle time. In practice you want somewhat more tasks than cores — the standard rule of thumb is roughly 10× as many tasks as processors.
A5. Task dependency graphs and the critical path
A task dependency graph is a directed acyclic graph in which nodes are tasks and an edge from A to B means B cannot start until A finishes. It is the precise statement of what may run in parallel.
The critical path is the longest chain of dependent tasks from start to finish. No matter how many processors you have, you can never finish faster than the critical path.
SUM 8 NUMBERS AS A TREE
a b c d e f g h
\/ \/ \/ \/ level 1: 4 adds, all parallel
ab cd ef gh
\ / \ /
\ / \ / level 2: 2 adds, both parallel
abcd efgh
\ /
\ / level 3: 1 add
abcdefgh
Work (total adds) = 7 -> time on 1 core
Critical path (levels) = 3 -> time on infinite cores
Maximum possible speedup = 7 / 3 = 2.33x
The ceiling was found before running anything. This is the graph-shaped form of Amdahl's Law — the "nine musicians cannot play a four-minute song in thirty seconds" idea from Unit I, made precise. A crucial corollary: speeding up a task that is not on the critical path changes nothing at all.
A6. Work, span and Brent's bound
Work, T₁ — the total amount of computation, equal to the time on one processor.
Span, T∞ — the critical-path length, equal to the time on infinitely many processors.
Work T1
Parallelism = ---------- = --------
Span T-inf
Lower bound (you can never beat): T_p >= max( T1/p , T_inf )
Upper bound (you are guaranteed): T_p <= T1/p + T_inf <- Brent's bound
"Parallelism" is also the maximum number of processors that
can ever be useful. Beyond it, extra processors do nothing.
Worked example — two numbers predict everything. A pipeline has work T₁ = 46 and span T∞ = 28. Parallelism = 46/28 = 1.64.
| Processors | T₁/p | T∞ | Which bound binds? | Best speedup |
|---|---|---|---|---|
| 1 | 46 | 28 | work — not enough processors | 1.0× |
| 2 | 23 | 28 | span — already | 1.64× |
| 8 | 5.8 | 28 | span | 1.64× |
| 1,000 | 0.05 | 28 | span | 1.64× |
From two processors onwards the span is in charge and nothing else matters — processors 3 through 1,000 change precisely nothing. Two numbers, computed on paper in five minutes, told you that before a line of code was written. This is the most useful single technique in the unit.
Part B — Measuring Parallel Performance
B1. Speedup
T(1) time on 1 processor
S(p) = -------- = --------------------
T(p) time on p processors
| T(1) | p | T(p) | Speedup | Verdict |
|---|---|---|---|---|
| 100 s | 4 | 25 s | 4.0× | Perfect / linear |
| 100 s | 4 | 40 s | 2.5× | Real life — overhead ate the rest |
| 60 s | 8 | 12 s | 5.0× | Good, not perfect |
Linear (perfect) speedup is p× on p cores. Always compare against the best serial algorithm, not against a crippled parallel program run on one core — that inflates the number dishonestly. Superlinear speedup (S > p) occasionally occurs, usually because the data now fits in the combined caches of p processors; it is a cache effect, not a violation of the bound.
B2. Efficiency
S(p) speedup
E(p) = -------- = -----------------
p number of cores
| Speedup | p | Efficiency | Reading |
|---|---|---|---|
| 3.2× | 4 | 80% | Solid — 20% lost to overhead |
| 2.5× | 4 | 63% | The missing 37% went to coordination |
| 7.6× | 8 | 95% | Excellent scaling |
Speedup asks "how much faster?"; efficiency asks "did I get what I paid for?" As cores are added, speedup rises while efficiency almost always falls. Falling efficiency is the early warning that overhead is beginning to dominate — and the signal to stop buying hardware.
B3. Amdahl's Law
Let f be the fraction of the program that is inherently sequential — reading input, acquiring a lock, combining results, coordination — and (1 − f) the fraction that parallelises perfectly.
DERIVATION — normalise T(1) = 1 and run on p cores
T(p) = f + (1 - f)/p serial stays f; parallel shrinks by p
1 1
S(p) = ----------------- = ------------------------
T(p) f + (1 - f)/p
As p -> infinity the parallel term vanishes:
1
S_max = -------- <-- the ceiling
f
The serial part refuses to shrink no matter how many cores you add. That single fact is the whole law.
B4. The ceiling, and how to measure f
| Serial fraction f | Parallel part | Smax = 1/f | Meaning |
|---|---|---|---|
| 50% | 50% | 2× | Half-serial code caps at 2× forever |
| 25% | 75% | 4× | |
| 10% | 90% | 10× | The Unit I example, proven |
| 5% | 95% | 20× | 95% parallel, yet capped at 20× |
| 1% | 99% | 100× |
Read the 5% row carefully. A program that is 95% parallel — which sounds excellent — can never exceed 20×, whether given 64 cores or a million. The last 5% is a brick wall.
Worked example — you cannot see f, so calculate it. Nobody hands you f. Run the program twice and solve for it. Measured: T(1) = 100 s and T(4) = 40 s. Should you buy a 64-core machine?
Step 1 — the speedup actually obtained
S(4) = 100 / 40 = 2.5 (not 4 — so something is serial)
Step 2 — substitute into Amdahl and solve for f
1 1
S = --------------- -> 2.5 = -------------
f + (1-f)/p f + (1-f)/4
1/2.5 = 0.4 = 0.75f + 0.25 -> 0.75f = 0.15 -> f = 0.20
Step 3 — the ceiling, before spending a rupee
S_max = 1/f = 1/0.20 = 5x, no matter what you buy.
| Cores | Predicted speedup | Runtime | Saved vs previous row |
|---|---|---|---|
| 1 | 1.00× | 100 s | — |
| 4 (measured) | 2.50× | 40 s | 60 s |
| 16 | 4.00× | 25 s | 15 s |
| 64 | 4.71× | 21.2 s | 3.8 s — for four times the machine |
Two timings and one equation turned Amdahl from an exam formula into a purchasing decision. The 64-core machine is not worth buying; reducing f is.
B5. Gustafson's Law
Amdahl carries a hidden assumption: the problem size stays fixed. In practice, given a bigger machine we do not run the same job faster — we run a bigger job. Weather models use a finer grid; AI trains on more data.
S(p) = p - f * (p - 1) f = serial fraction
Example, f = 0.20:
p = 16 -> S = 16 - 0.20 x 15 = 16 - 3 = 13x
p = 64 -> S = 64 - 0.20 x 63 = 64 - 12.6 = 51.4x
Amdahl with the same f caps at 5x forever.
Gustafson keeps climbing — near-linear, no ceiling — because
the parallel work grew to match the machine.
Amdahl vs Gustafson — both are right
| Amdahl | Gustafson | |
|---|---|---|
| Question asked | "I have this problem. How much faster can I make it?" | "I have a bigger machine. How much bigger a problem can I solve in the same time?" |
| Problem size | Fixed | Grows with p |
| Result | Hard ceiling at 1/f | Near-linear speedup, no ceiling |
| Formula | S = 1 / (f + (1−f)/p) | S = p − f(p − 1) |
| Corresponds to | Strong scaling | Weak scaling |
They do not contradict each other; they answer different questions. Most large-scale real-world computing — weather, AI, simulation — lives in Gustafson's world, which is precisely why supercomputers keep getting bigger and keep being worth building.
B6. Strong vs weak scaling
| Strong scaling | Weak scaling | |
|---|---|---|
| Problem size | Fixed | Grows in proportion to p |
| Question | Same job, more cores → faster? | Bigger job plus more cores → same time? |
| Governed by | Amdahl's Law | Gustafson's Law |
| Ideal result | Time drops as 1/p | Time stays constant as p grows |
| Real example | Speed up one fixed image render | 10× the data on 10× the GPUs, same wall-clock |
Rule of thumb: "problem stays the same size" → strong scaling → Amdahl. "Problem grows with the machine" → weak scaling → Gustafson.
B7. Where the missing speedup goes
Amdahl's f gives a theoretical floor. Real machines do worse, because the clean formula ignores overhead:
| Source | What it is | Behaviour |
|---|---|---|
| Communication | Cores and nodes exchanging data | Grows with worker count — often the number one killer at scale |
| Synchronisation | Locks and barriers | Every barrier makes the fastest core wait for the slowest |
| Load imbalance | One core receives more work; the rest idle | The job runs at the speed of the slowest worker |
| Parallel overhead | Spawning threads or processes, scheduling, splitting and merging data | Fixed cost per parallel region — punishing for fine-grained work |
Worked example — account for every missing second. A 120-second job is parallelised across 8 cores. You hoped for 15 s; you measured 28.6 s, a speedup of 4.2×.
| Where the time went | Seconds | Why it exists | Fix |
|---|---|---|---|
| Useful parallel work | 13.5 | 108 s of work ÷ 8 cores | — this is the point |
| Serial section (f = 10%) | 12.0 | Reading input, writing the final file | Overlap I/O, or parallelise the read |
| Communication | 1.8 | Cores exchanging partial results | Bigger tasks, fewer exchanges |
| Load imbalance | 0.9 | One core got a heavier chunk; seven waited | Dynamic scheduling → Part C |
| Thread and sync overhead | 0.4 | Fork, join, barriers | Fewer parallel regions |
| Total | 28.6 | Speedup = 120 / 28.6 = 4.2× |
Note the second row: the serial 12 seconds is now the largest single item — larger than all the actual parallel work combined. On one core it was 10% of the runtime and invisible; on eight cores it is 42% and dominant. Parallelising the code makes the serial part relatively bigger, not smaller.
Part C — Load Balancing
C1. Balance the cost, not the count
Real tasks have uneven cost: compressing a 2 KB file versus a 2 GB file; testing whether 7 is prime versus a ten-digit number. Splitting by count leaves some workers swamped and others idle. A parallel job finishes only when its slowest worker finishes, so idle time is pure waste.
4 workers, 8 uneven tasks. Numbers = cost in seconds.
Tasks: [1] [1] [1] [1] [5] [5] [5] [5]
BAD split — 2 tasks each, in order: GOOD split — balance the cost:
W1: [1][1] = 2s ...idle 8s... W1: [5][1] = 6s
W2: [1][1] = 2s ...idle 8s... W2: [5][1] = 6s
W3: [5][5] = 10s <-- everyone W3: [5][1] = 6s
W4: [5][5] = 10s waits for these W4: [5][1] = 6s
Wall-clock: 10s Wall-clock: 6s -> 40% faster
Same eight tasks, same four workers, no new hardware — a smarter split is 40% faster.
C2. Static vs dynamic load balancing
| Static | Dynamic | |
|---|---|---|
| Decided | Up front, once, before execution | On demand, as workers become free |
| Mechanism | Divide all work in advance and hand each worker its share | All tasks in a shared queue; workers take the next one when free |
| Overhead | None | Queue access on every task |
| Best for | Uniform, predictable task costs | Irregular, unpredictable task costs |
| Fails when | Cost estimates are wrong — the assignment is locked in | Tasks are too small — queue overhead dominates |
Worked example — count the idle time. Eight tasks, four cores. Costs: 10, 2, 3, 12, 1, 8, 2, 4 seconds — total 42 s. Perfect balance would give 42 ÷ 4 = 10.5 s.
STATIC — two tasks each, handed out up front
0 4 8 12 16
|----|----|----|----|
C0 [ T1: 10 ][T5]........
C1 [T2][ T6: 8 ]........
C2 [T3][T7]................ <-- idle 11 s!
C3 [ T4: 12 ][ T8: 4 ]
finish = 16 s
DYNAMIC — grab the next task when free
0 4 8 12 16
|----|----|----|----|
C0 [ T1: 10 ]....
C1 [T2][T5][T7][T8 ]..
C2 [T3][ T6: 8 ]..
C3 [ T4: 12 ]
finish = 12 s
| Strategy | Finish | Speedup | Core-seconds wasted | Efficiency |
|---|---|---|---|---|
| Static | 16 s | 2.6× | 22 s idle (34%) | 66% |
| Dynamic | 12 s | 3.5× | 6 s idle (12%) | 88% |
| Perfect balance (unattainable) | 10.5 s | 4.0× | 0 | 100% |
Same tasks, same cores, same total work — a 33% difference in wall-clock from scheduling policy alone. Note also that even dynamic scheduling cannot reach 10.5 s, because task T4 alone takes 12 s: no schedule can finish faster than its single longest task, which is the critical path of A5 reappearing.
C3. Work stealing
Dynamic scheduling fixes imbalance but needs a shared task queue — and at 64 cores that queue becomes the bottleneck, exactly as the shared bus did in Unit II. Work stealing gives every worker its own queue. A worker takes from its own queue with no contention; when its queue empties it becomes a thief and takes a task from a randomly chosen busy worker.
Each thread owns a DEQUE. It works from one end; thieves take the other.
t=0 T0: [A B C D] T1: [E F] T2: [ ] T3: [G]
^own end all busy
t=1 T2 is EMPTY. It picks a victim at random -> T0.
T2 steals from the FAR end of T0's deque:
T0: [A B C] T1: [E F] T2: [D] T3: [G]
no lock needed on T0's own end
t=2 T3 finishes G, empties, steals from T0 (far end again):
T0: [A B] T1: [E F] T2: [D] T3: [C]
t=3 Every thread busy again. T0 was never interrupted.
Why two ends, and why that choice matters:
- A thread takes its own work from the near end — the most recently pushed task, whose data is still hot in its cache.
- A thief takes from the far end — the oldest task, which in a divide-and-conquer computation is usually the largest, so one steal buys a lot of work and steals are rare.
- Because the two ends are different, the owner and the thief rarely contend, so no lock is needed in the common case.
This is how real schedulers work: Java's ForkJoinPool, Go's goroutine scheduler, Rust's Rayon, Intel TBB and Cilk all use work stealing. Know the term — it is the answer to "how do you get dynamic balance without a central bottleneck?"
C4. Embarrassingly parallel vs tightly coupled
| Embarrassingly parallel | Tightly coupled | |
|---|---|---|
| Definition | Independent tasks with little or no communication. Split, run, collect. | Tasks communicate constantly; each step needs results from neighbours. |
| Examples | Rendering movie frames, prime testing, Monte Carlo simulation, matrix multiply | Physics on a grid, most sorting algorithms, graph algorithms |
| Scaling | Near-linear | Communication becomes the bottleneck |
| Load balancing | Trivial | Must account for the communication pattern too |
This single classification predicts how hard a parallel problem will be before you write a line of code.
C5. Case studies
| Problem | Character | Strategy | The catch |
|---|---|---|---|
| Parallel search | Independent, exploratory | Split the space; first worker to find a hit signals the rest to stop | Finish time depends on luck; needs a stop signal, else workers keep searching pointlessly |
| Matrix multiply | Embarrassingly parallel — every output cell independent | Give each worker a band of rows. Static balancing is perfect since cost is uniform | Not correctness but memory: reading B column-by-column is cache-hostile (Unit II). Real code uses blocking/tiling |
| Sorting | Tightly coupled — any element may move relative to any other | Divide and conquer: sort chunks in parallel, then merge | The merge is the serial fraction — speedup is good but sub-linear |
| Prime counting | Independent but highly uneven cost | Dynamic scheduling; testing a large number costs far more than a small one | Static splitting by range gives the last worker all the expensive numbers |
PARALLEL MATRIX MULTIPLY — why it is the poster child
C[i][j] = A[i][0]*B[0][j] + A[i][1]*B[1][j] + ... + A[i][n-1]*B[n-1][j]
for i in 0..n:
for j in 0..n: <-- every C[i][j] is INDEPENDENT
C[i][j] = dot(row i of A, col j of B)
Workers only READ A and B and WRITE their own cells -> no race at all.
Uniform cost -> static balancing is optimal.
THREE PARALLEL SORTS worth naming:
Odd-even transposition — compare disjoint neighbour pairs; all
compares within a phase run at once
Parallel merge sort — sort halves in parallel, then merge
(the merge is the serial-ish part)
Bitonic sort — a regular sorting network; more compares,
but maps perfectly onto a GPU
C6. Choosing a strategy
| Ask… | If YES | If NO |
|---|---|---|
| Do all tasks cost the same? | Static | Dynamic |
| Are tasks independent? | Embarrassingly parallel — easy | Tightly coupled — mind the communication |
| Are tasks tiny? | Agglomerate into coarser chunks | Fine chunks are fine |
| Is task cost unpredictable? | Dynamic plus work stealing | Static is simplest |
Applied: matrix multiply is uniform and independent → static, easy. Prime counting is uneven but independent → dynamic. Sorting is tightly coupled → divide and conquer, mind the merge. Search is independent with luck-dependent finish → dynamic plus a stop signal.
Quick recall
Formulas — this unit is mostly formulas, learn them cold
Speedup S(p) = T(1) / T(p)
Efficiency E(p) = S(p) / p
Amdahl S(p) = 1 / ( f + (1-f)/p )
Amdahl ceiling S_max = 1 / f (as p -> infinity)
Gustafson S(p) = p - f(p - 1)
Work / Span parallelism = T1 / T_inf
Lower bound T_p >= max( T1/p , T_inf )
Brent's bound T_p <= T1/p + T_inf
f = serial fraction. T1 = work. T_inf = span = critical path.
The definitions you must be able to state
- PCAM — Partition, Communication, Agglomeration, Mapping. Expand, then contract.
- Granularity — task size, i.e. the computation-to-communication ratio. Too fine → overhead; too coarse → imbalance.
- Task dependency graph — a DAG whose edges are "must finish before".
- Critical path — the longest chain of dependent tasks; an absolute lower bound on runtime.
- Work T₁ — time on one processor. Span T∞ — time on infinite processors. Parallelism — T₁/T∞, the most processors that can ever help.
- Speedup — T(1)/T(p). Efficiency — S(p)/p. Linear speedup — S = p.
- Amdahl's Law — fixed problem, hard ceiling 1/f. Gustafson's Law — growing problem, near-linear speedup.
- Strong scaling — fixed problem, more cores. Weak scaling — problem grows with cores.
- Static balancing — assignment decided up front. Dynamic balancing — workers pull from a shared queue.
- Work stealing — per-worker deques; idle workers steal from the far end of a busy worker's deque.
- Embarrassingly parallel — independent tasks, negligible communication. Tightly coupled — constant communication.
Self-check
Answer each out loud before opening it.