Chapter 33 — Key Takeaways (OpenMP)
A one-page reference for shared-memory parallelism with OpenMP directives. Scan it; the data-scoping table is the one to internalize.
The mental model: fork–join
master --serial--> !$omp parallel [FORK team] --all threads run--> !$omp end parallel [JOIN] --serial-->
Add parallelism one region at a time. Directives are comments (!$omp …): no -fopenmp ⇒ correct serial
program; -fopenmp ⇒ parallel.
Directives introduced
| Directive | What it does |
|---|---|
!$omp parallel … !$omp end parallel |
Fork a team; every thread runs the block. |
!$omp do … !$omp end do |
Work-share a loop's iterations across the team (inside a parallel region). |
!$omp parallel do … !$omp end parallel do |
Fork + work-share a loop in one directive (the common case). |
!$omp sections / !$omp section |
Give different code blocks to different threads (task parallelism). |
!$omp workshare |
Parallelize whole-array Fortran statements (unevenly optimized; prefer do). |
!$omp barrier |
All threads wait until every thread arrives. |
!$omp critical … !$omp end critical |
One thread at a time (general mutual exclusion; relatively slow). |
!$omp atomic |
Make the single next x = x op expr update indivisible (cheap). |
!$omp simd` / `!$omp do simd |
Vectorize a loop within a thread / thread + vectorize together. |
!$omp task … !$omp end task, !$omp taskwait |
Queue irregular/recursive units of work for the team. |
!$omp single |
Exactly one thread runs the block (others skip to the implicit barrier). |
Runtime library (use omp_lib)
| Call | Returns |
|---|---|
omp_get_thread_num() |
This thread's id, 0 … nthreads-1 (master = 0). |
omp_get_num_threads() |
Team size in the current region. |
omp_get_max_threads() |
Threads a parallel region would use. |
omp_set_num_threads(n) |
Set the default team size. |
omp_get_wtime() |
Wall-clock seconds (for timing). |
Data scoping — the heart of the chapter
| Attribute | Meaning | Use for |
|---|---|---|
shared(x) |
One instance, all threads see it | Read-only data; distinct-cell output arrays |
private(x) |
One uninitialized copy per thread | Loop indices, scratch temporaries |
firstprivate(x) |
Private, initialized to the pre-region value | Scratch that must start from a set value |
lastprivate(x) |
Private; last iteration's value copied out | When the final scratch value must survive |
reduction(op:x) |
Private per thread, combined by op at the end |
Running sum/product/max/min/count |
default(none) |
Force explicit scoping of every variable | Always. Turns a silent race into a compile error. |
Rules of thumb: read-only → shared; scratch & loop indices → private; per-thread result to combine →
reduction. The outer (!$omp do) index is auto-private; inner indices and temporaries are your job.
The archetypal bug, and its cure
! WRONG: data race -- s read-add-written by all threads, wrong & run-to-run-varying
!$omp parallel do shared(s, x) private(i)
do i = 1, n; s = s + x(i); end do
! RIGHT: private partial sums, combined once at the end -- correct on any thread count
!$omp parallel do default(none) shared(x) private(i) reduction(+:s)
do i = 1, n; s = s + x(i); end do
Reduction identities (each thread's private copy starts here)
op |
+ |
* |
max |
min |
.and. |
.or. |
|---|---|---|---|---|---|---|
| identity | 0 | 1 | −∞ (-huge) |
+∞ (huge) |
.true. |
.false. |
Combine per-thread results: prefer, in order
reduction (no contention) → atomic (one cheap indivisible update) → critical (general but
serializing). A critical in a hot loop reintroduces Amdahl's serial fraction.
Scheduling — schedule(kind[,chunk])
| Kind | Assignment | Best for |
|---|---|---|
static |
Equal contiguous chunks, decided up front; reproducible | Uniform work (the stencil) |
dynamic |
Grab-a-chunk at run time; self-balancing | Uneven / lumpy work |
guided |
Like dynamic, shrinking chunks | Uneven work, less hand-out overhead |
Pitfalls (all silent — no crash, no warning)
- The shared-accumulator race — write to one shared scalar in a parallel loop ⇒ wrong, nondeterministic.
Use
reduction. - Unscoped inner index / temporary — the inner
jand anyrowsum-style scratch must beprivate.default(none)catches it at compile time. - Work-sharing with no team —
!$omp dooutside a parallel region runs serially; no parallelism. - False sharing — threads writing adjacent slots of a per-thread array share a cache line ⇒ correct but
slow. Use
reduction, or pad to separate cache lines. - Tiny regions — forking costs ~microseconds; a region doing little work runs slower parallel. Make regions big; hoist them out of the hot loop.
Compile flag introduced
$ gfortran -std=f2018 -fopenmp -Wall prog.f90 -o prog
$ OMP_NUM_THREADS=4 ./prog
-fopenmp both activates !$omp directives and links the runtime (omp_lib). Set the team size at run time
with OMP_NUM_THREADS — same binary, 1 thread on a laptop or 64 on a node.
Numbers/rules worth memorizing
- Master thread id = 0; ids run
0 … nthreads-1. default(none)on every region. The compiler's default guess (shared) is the bug.- Determinism split: the result is deterministic (must match the serial answer); the schedule is not.
staticis reproducible;dynamic/guidedare not.
Project piece added this chapter
step(field, alpha, dt) — same frozen interface, now with !$omp parallel do over the interior sweep:
fields shared, loop indices private, static schedule. The two-buffer FTCS structure (old field read,
new buffer written) makes the sweep embarrassingly parallel, so the OpenMP result equals the
Chapter 24 result on any thread
count. Coarray and MPI variants of the same step follow in Chapters 32 and 34. Full directive/clause
reference: Appendix G.