Parallel Programming · lesson 8 of 8
Beginner
2Intermediate
5- 3 Race Conditions: A Sum That Gives Three Different Wrong Answers
- 4 Atomics vs Locks, and the Race Hiding Inside a Check-Then-Act
- 5 MPI Basics: a Real Deadlock That Only Shows Up at Scale
- 6 MPI Collectives: Why MPI_Reduce Leaves Every Rank but One Empty
- 7 OpenMP vs MPI, Combined: a Hybrid Program and a Real Oversubscription Penalty
Also part of Parallel Programming
2Measuring Real Speedup Against Amdahl's Ceiling
12x the threads gets 2.88x the speedup, not 12x — a fixed serial portion sets a hard ceiling Amdahl's Law predicts, and the real measured speedup falls short of even that idealized prediction.
By SkillByExample · Published
Every earlier lesson in this track measured a real improvement from parallelism. This one measures the point where more threads stop helping — and shows that even the theoretical ceiling for how much they should help is itself an optimistic upper bound.
A program with a fixed serial cost and a scalable parallel cost
// amdahl.c
#include <stdio.h>
#include <stdlib.h>
#include <omp.h>
int main(int argc, char** argv) {
int nthreads = atoi(argv[1]);
omp_set_num_threads(nthreads);
long n = 800000000L;
double serial_result = 0;
double t0 = omp_get_wtime();
// Fixed serial setup: always runs on one thread, regardless of nthreads
for (long i = 0; i < 200000000L; i++) {
serial_result += 1.0 / (1.0 + (i % 13));
}
double t_serial = omp_get_wtime();
// Parallel portion: this part actually scales with thread count
double parallel_result = 0;
#pragma omp parallel for reduction(+:parallel_result)
for (long i = 0; i < n; i++) {
parallel_result += 1.0 / (1.0 + (i % 997));
}
double t1 = omp_get_wtime();
printf("threads=%2d serial_part=%.3fs parallel_part=%.3fs total=%.3fs\n",
nthreads, t_serial - t0, t1 - t_serial, t1 - t0);
return 0;
}
gcc -fopenmp -O2 -o amdahl amdahl.c
./amdahl 1
threads= 1 serial_part=0.411s parallel_part=1.726s total=2.136s
Measuring speedup as thread count scales
./amdahl 2
./amdahl 4
./amdahl 8
./amdahl 12
./amdahl 24
threads= 2 serial_part=0.434s parallel_part=1.177s total=1.611s
threads= 4 serial_part=0.367s parallel_part=0.726s total=1.092s
threads= 8 serial_part=0.334s parallel_part=0.485s total=0.819s
threads=12 serial_part=0.332s parallel_part=0.411s total=0.742s
threads=24 serial_part=0.322s parallel_part=0.459s total=0.781s
The serial part stays essentially flat (0.33-0.43s) at every thread count, exactly as it
should — nothing about it can parallelize. The parallel part keeps shrinking as threads
increase, but the serial part’s fixed cost matters more and more as it does.
| Threads | Total time | Measured speedup | Amdahl-predicted speedup |
|---|---|---|---|
| 1 | 2.136s | 1.00x | 1.00x |
| 2 | 1.611s | 1.33x | 1.68x |
| 4 | 1.092s | 1.96x | 2.54x |
| 8 | 0.819s | 2.61x | 3.41x |
| 12 | 0.742s | 2.88x | 3.85x |
| 24 | 0.781s | 2.73x | 4.42x |
Speedup climbs from 1.00x to 2.88x as threads go from 1 to 12 — a 12x increase in
threads bought only a 2.88x increase in speed. Going further, to 24 threads, made it
worse (2.73x), not better.
Where the Amdahl-predicted column comes from
The serial fraction at 1 thread is 0.411 / 2.136 ≈ 19% — plugging that into Amdahl’s formula,
speedup(N) = 1 / ((1-p) + p/N) with p ≈ 0.81, gives the predicted column above. As N
grows without bound, that formula approaches 1 / 0.19 ≈ 5.2x — a hard ceiling this program
can never exceed no matter how many threads it’s given, because roughly a fifth of its total
work is permanently stuck running on one thread.
Notice that measured speedup falls short of the Amdahl prediction at every single thread
count — 2.88x measured versus 3.85x predicted at 12 threads, for instance. Amdahl’s
formula only accounts for the serial-vs-parallel split; it assumes the parallel portion scales
with zero overhead. Real thread creation, the reduction clause’s combining step, and cache
effects between cores are all real costs the idealized formula doesn’t model — so measured
results are reliably worse than the prediction, never better.
Why 24 threads made things worse, which Amdahl’s formula can’t explain at all
Amdahl’s Law has no concept of a physical core count — it models N as an abstract number of
available processors. This machine has 12 real cores; 24 threads is 2x oversubscription, which
the earlier oversubscription lesson already showed carries a real, measured penalty. Amdahl’s
ceiling (~5.2x) and this machine’s hardware limit (12 real cores) are two separate limits,
and this program’s speedup curve bends because of the hardware limit well before Amdahl’s
theoretical ceiling would have mattered on its own.
Practice
1. Compute the Amdahl-predicted speedup at 2 threads by hand and confirm it against the table.
speedup(2) = 1 / ((1 - 0.81) + 0.81/2) = 1 / (0.19 + 0.405) = 1 / 0.595 ≈ 1.68
Matches the table’s 1.68x for 2 threads — the formula only needs the measured serial
fraction (~19%) and the thread count; everything else follows directly from those two
numbers.
2. Reduce the fixed serial portion and predict what happens to the achievable ceiling.
Halving the serial loop’s iteration count (to 100000000L) would roughly halve the measured
serial fraction to ~10%, raising Amdahl’s ceiling from 1/0.19 ≈ 5.2x to 1/0.10 = 10x. A
smaller serial fraction directly raises the theoretical maximum speedup — this is the whole
reason real HPC code spends real engineering effort minimizing genuinely serial setup and
I/O, not because it’s slow in isolation, but because of how hard a ceiling it puts on
everything downstream of it.
3. Explain why the 24-thread run's serial_part measurement (0.322s) is close to the 1-thread run's (0.411s) despite running with far more threads available.
The serial loop in amdahl.c has no #pragma omp on it at all — it always runs on a single
thread regardless of nthreads, so its timing shouldn’t depend on thread count in principle.
The small variation between runs (0.411s down to 0.322s) is measurement noise (cache
state, OS scheduling jitter) rather than a real effect of thread count — a useful reminder that
even a “fixed” cost isn’t perfectly identical run to run on real hardware, only approximately
constant.
That closes the track. The thread through all eight lessons: parallelism only does what you explicitly divide, protect, or combine — and even done correctly, how much it can help is bounded by both a program’s own serial fraction and the real hardware underneath it, neither of which more threads alone can fix.