"OpenMP threading in Fortran. Use whenever you write `!$omp parallel do`, `simd`, `reduction`, or `maxloc`, hit a race, wrong answer, or an `-fopenmp` build error in a Fortran loop."
Install
npx skillscat add spcl/hpcagent-bench/openmp-fortran Install via the SkillsCat registry.
openmp-fortran
Grading is MULTI-CORE, baseline SERIAL, -fopenmp always on. Never hardcode a thread count --
the grading machine presets OMP_NUM_THREADS; read omp_get_max_threads() (use omp_lib).
(!$omp simd cannot sit on a do concurrent; lang-fortran has that spelling.)
A directive is an assertion, not a request
parallel do claims the iterations may run in any order. simd claims the lanes may. Nothing
checks either claim; both are believed. Assert wrongly and you get a RACE -- a wrong answer, not a
slow one, and one that looks plausible.
So derive the dependence vectors FIRST and name the axis each one crosses (lang-fortran has the
test, the rewrites that free an axis, and the stride question that decides whether threading pays
at all). That axis may not be threaded; any axis it does not cross may. Misfiling returnscorrect: false and costs a round trip.
If the comment you are about to write names a dependence, the directive is wrong. The tell:
! the recurrence relation requires sequential processing <- correct diagnosis
!$omp simd <- contradicts it
do i = 3, n
a(i) = a(i - 2) + x(i)
end doThe four bins
PARALLEL -- every write lands at this iteration's own subscript, no scalar carries state.!$omp parallel do simd on the OUTERMOST such loop: threads across cores, lanes within each. A
tiny trip count loses to the cost of forking the team.
REDUCTION -- the only carried state is an accumulator (sum, max, min, count). Same directive
plus reduction(+:s) (or max:, min:). Never a shared scalar, never hand-built per-thread
arrays. The clause also authorizes the FP reassociation the compiler refuses on its own, and the
graded tolerance covers it.
Max or min WITH ITS INDEX -- Fortran has this as an INTRINSIC. maxloc(v) returns the
position directly, one line, already 1-based, which is the base the judge expects for an index
output:
k = maxloc(v, dim=1)Reach for it FIRST and time it. reduction(max:m) gives you the value and loses the position, and
recovering the position afterwards costs a second pass -- often more than the intrinsic costs in
the first place. Only if the intrinsic measures slower is a hand-written parallel form worth
writing, and then the tie-break must go to the SMALLER index or the answer disagrees with a serial
sweep.
RECURRENCE -- the written array is read at ANOTHER iteration's subscript. The loop carrying
the chain stays serial; threading it races. That does not make the NEST serial: thread an axis the
chain does not cross, or reshape the nest so there is one (lang-fortran). Prefix sum is the one
recurrence with a directive of its own; statements before the scan feed the sum, statements
after read the scanned value:
s = 0.0d0
!$omp parallel do simd reduction(inscan, +:s)
do i = 1, n
s = s + c(i)
!$omp scan inclusive(s)
x(i) = s
end doexclusive(s) is the value-before-this-iteration variant. Scans reassociate; tolerance applies.
The clause is gfortran-only. flang rejects it outright -- not yet implemented: Unhandled
clause reduction with modifier. So if you select the LLVM compiler the clause is not available
and you write the two passes yourself: each thread sums its own contiguous chunk, one thread
prefix-sums the per-chunk totals, then each thread re-walks its chunk starting from that offset.
Same answer as the serial sweep on either compiler. Pick the compiler before you pick the
spelling.
SCATTER -- writes through an index array, a(idx(i)). If the task guarantees distinct
indices it is PARALLEL, no atomics. Only DUPLICATE indices collide: then per-thread copies merged
after the loop (usually fastest), or !$omp atomic on the update (often slower than serial).
Fork and barrier cost
A worksharing construct ends with an IMPLICIT BARRIER. So !$omp parallel wrapped around an outer
sequential loop with !$omp do inside pays one barrier per outer iteration: every thread waits for
the slowest share of the row before any of them starts the next one, and that wait is charged as
many times as the outer loop runs.
- Default spelling: ONE combined
parallel doover the whole iteration space -- the team forks
once and synchronizes once.collapse(n)when the outer loop alone is too short to fill the
cores: exactly n PERFECTLY nested loops, nothing between thedostatements. nowaiton the innerdowhen what follows does not read what it wrote. Reach for theparallel+doshape only when the region genuinely holds work BETWEEN the loops.
Making a legal directive pay
schedule(static)unless the per-iteration cost genuinely varies: it hands each thread one
WIDE contiguous span, and a narrow strip breaks the memory stream and gives most of the win
back. A dynamic schedule buys load balance with per-chunk bookkeeping.- Keep per-thread partials a cache line apart, or the threads fight over one line (false sharing);
combine them after the loop. declare simdon a helper called from the hot loop, else the call is a vectorization barrier.!$omp unroll partial(4)on the INNER loop of a nest you already thread -- neverfull:
it deletes the loop the worksharing directive above needs. gfortran only -- flang rejects it
(PARTIAL clause is not allowed on directive UNROLL), and raising the version only moves the
failure into lowering.- Split the construct when the shape demands it:
parallel doon the outer loop,simd
alone on the unit-stride inner one.
Clauses
shared(x): one object, all threads -- right for input/output arrays, a RACE when two threads
write one scalar. private(x): own UNINITIALIZED copy -- every scratch scalar the body writes.firstprivate / lastprivate: private, initialized from entry / sequentially-last value copied
back out. reduction(op:x): what a sum/max/count wants. Getting sharing wrong is a race,
not a build error. Loop indices are
private already.
Build errors that cost a turn
- End the loop with
end doand write nothing after it. The closing directive is optional
and omitting it is always safe; if you write one it must name the SAME construct token for
token: opening!$omp parallel do simdand closing!$omp end parallel dodrops thesimd
and is a BUILD ERROR -- and gfortran blames the closing line. aligned(...)is unavailable on ABI dummies (must be POINTER, ALLOCATABLE, Cray pointer
or C_PTR) -- rejected outright.!$omp worksharedoes NOT thread on gcc (gfortran lowers it tosingle). Rewrite array
syntax as an explicit loop underparallel do.- Skip
default(none). The one variable you miss is always the accumulator -- which belongs
inreduction(...)anyway. - No
exit/cycleto an outer loop, noreturn, out of a threaded loop. A search loop
keeps its trip count and reduces instead:reduction(min:first)over a per-iteration
candidate. nowaitdoes not exist on a combinedparallel do;scheduleis worksharing-only (on a baresimdit is a build error).