Fortran parallel loops
last modified November 14, 2026
A loop whose iterations do not depend on each other can be split across the
cores of a processor, because the order of the iterations does not matter. The
Fortran 2008 standard added a construct for exactly this promise:
do concurrent looks like a do loop but tells the
compiler that every iteration is independent, so it may reorder them, run them
together, or hand them to another processor. The programmer states the
independence; the compiler decides what to do with the statement.
This article writes loops with do concurrent, checks the rules the
compiler enforces, shows the accumulator mistake that the construct cannot
protect you from, and then compares what gfortran 13 actually does: it accepts
the construct and runs it serially, and even
-ftree-parallelize-loops leaves it alone. The same flag does
parallelize an ordinary do loop whose body is simple enough, which
is measured and confirmed in the compiler's own dump file.
The construct
The syntax is the counted loop with the word concurrent after
do, and it accepts several indices separated by commas. The
iteration count must be known before the loop starts, and the index is a
variable of integer type that the construct controls. Inside it, the program
writes the result of each iteration to a place that belongs to that iteration,
such as one element of an array.
program dc1
implicit none
integer, parameter :: n = 10
integer :: squares(n), table(3, 3)
integer :: i, j
do concurrent (i = 1:n)
squares(i) = i * i
end do
print '(a, 10i5)', 'squares: ', squares
print '(a, i0)', 'their sum: ', sum(squares)
do concurrent (i = 1:3, j = 1:3)
table(i, j) = 10 * i + j
end do
print '(a)', 'a two-index table:'
do i = 1, 3
print '(3i4)', table(i, :)
end do
print '(a, i0)', 'the cube of 7: ', cube(7)
contains
pure integer function cube(x) result(y)
integer, intent(in) :: x
y = x * x * x
end function cube
end program dc1
The first loop fills ten elements with their squares, and the sum on the next
line confirms the contents without depending on the order in which they were
written. The second loop has two indices and fills a three-by-three table, the
form in which a grid or a matrix is initialised. The call to cube
sits outside the construct, but the same function could be called inside it,
because it is pure: it reads its argument and writes nothing else.
That restriction is what makes a procedure safe to run in several iterations at
once.
$ gfortran dc1.f90 -o dc1 $ ./dc1 squares: 1 4 9 16 25 36 49 64 81 100 their sum: 385 a two-index table: 11 12 13 21 22 23 31 32 33 the cube of 7: 343
Iterations that do not touch each other
A smoothing pass over an array shows the pattern clearly: each new value
depends on three old values, and on nothing that this pass writes. The loop
therefore reads from one array and writes to another, and the two arrays are
swapped after each pass. If the loop read and wrote the same array, an iteration
would see a value that a neighbouring iteration had already changed, and the
result would depend on the order of execution, which is exactly what
do concurrent forbids.
program dc2
implicit none
integer, parameter :: n = 12, passes = 3
real :: old(n), new(n), serial(n), serial_new(n), start(n)
integer :: i, k
start = 0.0
start(6) = 100.0
old = start
serial = start
do k = 1, passes
new = old
do concurrent (i = 2:n - 1)
new(i) = 0.25 * old(i - 1) + 0.5 * old(i) + 0.25 * old(i + 1)
end do
old = new
serial_new = serial
do i = 2, n - 1
serial_new(i) = 0.25 * serial(i - 1) + 0.5 * serial(i) + &
0.25 * serial(i + 1)
end do
serial = serial_new
print '(a, i0, a, 12f5.1)', 'pass ', k, ': ', old
print '(a, es10.2)', 'the largest difference from the loops: ', &
maxval(abs(old - serial))
end do
end program dc2
A peak of 100 in the middle of the row spreads outwards by a quarter of the difference at every pass, and the profile becomes smoother each time. The serial loop beside it uses its own pair of arrays and produces exactly the same numbers, which the last line of every pass confirms with a difference of zero. The comparison is the useful habit: an independent-looking loop is best trusted after it has been checked against a version whose order is fixed.
$ gfortran dc2.f90 -o dc2 $ ./dc2 pass 1: 0.0 0.0 0.0 0.0 25.0 50.0 25.0 0.0 0.0 0.0 0.0 0.0 the largest difference from the loops: 0.00E+00 pass 2: 0.0 0.0 0.0 6.2 25.0 37.5 25.0 6.2 0.0 0.0 0.0 0.0 the largest difference from the loops: 0.00E+00 pass 3: 0.0 0.0 1.6 9.4 23.4 31.2 23.4 9.4 1.6 0.0 0.0 0.0 the largest difference from the loops: 0.00E+00
The accumulator trap
The most common mistake with do concurrent is a sum written inside
it. Adding to one variable from every iteration creates a dependence: two
iterations read total, add their term and write it back, and one of
the writes is lost. The construct does not detect this, because the rule is on
the programmer, and a compiler that runs the loop serially produces the right
answer anyway, which hides the bug until the day the loop is actually split.
program dc3
implicit none
integer, parameter :: n = 1000
integer :: a(n), parts(n), i, total
do concurrent (i = 1:n)
a(i) = mod(i, 7)
end do
total = 0
do concurrent (i = 1:n)
total = total + a(i)
end do
print '(a, i0)', 'accumulated inside the construct: ', total
print '(a, i0)', 'sum afterwards: ', sum(a)
do concurrent (i = 1:n)
parts(i) = a(i)
end do
print '(a, i0)', 'a per-iteration array: ', sum(parts)
end program dc3
All three lines print 3003, and the first of them is the one to distrust: it
is correct only because this compiler executed the iterations in order. The
second line reaches the same number through sum, a reduction that
the compiler is allowed to implement in any order, and the third stores one
partial value per iteration and adds them afterwards. Either of the last two is
safe; the first is a program whose result depends on a promise the loop does not
keep.
$ gfortran dc3.f90 -o dc3 $ ./dc3 accumulated inside the construct: 3003 sum afterwards: 3003 a per-iteration array: 3003
What the compiler rejects
Two rules are checked at compile time, and both protect the independence of the
iterations. A procedure called inside the construct must be pure,
because an impure function could change a module variable and make one
iteration affect another. And the loop cannot be left early: exit
and cycle name an order of execution, which the construct does not
have.
module impure_module
implicit none
contains
real function twice(x)
real, intent(in) :: x
twice = 2.0 * x
end function twice
end module impure_module
program bad_pure
use impure_module
implicit none
integer :: i
real :: a(4)
do concurrent (i = 1:4)
a(i) = twice(real(i))
end do
print *, a
end program bad_pure
The function twice is ordinary, and gfortran refuses to call it
from the loop with a message that names the reason. Adding the
pure prefix to the function, and intent(in) to its
argument, is all that the corrected program needs.
program bad_exit
implicit none
integer :: i, a(4)
do concurrent (i = 1:4)
if (i == 2) exit
a(i) = i
end do
print *, a
end program bad_exit
The second program tries to stop after the second iteration, and the compiler
rejects the exit with the same kind of message. Both errors appear
in the transcript below, which also compiles the corrected version.
program dc4
use iso_fortran_env, only: real64
implicit none
integer, parameter :: n = 6
real(real64) :: a(n)
integer :: i
do concurrent (i = 1:n)
a(i) = decay(real(i, real64))
end do
print '(a, 6f8.4)', 'the array: ', a
print '(a, f0.6)', 'its sum: ', sum(a)
contains
pure real(real64) function decay(x) result(y)
real(real64), intent(in) :: x
y = exp(-x) * cos(x)
end function decay
end program dc4
The corrected loop calls a pure function of a real64 argument and
stores each result in its own element, and the values and their sum are printed
afterwards. The compiler now accepts the program without a word.
$ gfortran -std=f2018 -Wall -Wextra bad_pure.f90 -o bad_pure
bad_pure.f90:18:15:
18 | a(i) = twice(real(i))
| 1
Error: Reference to impure function ‘twice’ at (1) inside a DO CONCURRENT block
$ gfortran -std=f2018 -Wall -Wextra bad_exit.f90 -o bad_exit
bad_exit.f90:6:24:
6 | if (i == 2) exit
| 1
Error: EXIT statement at (1) leaves DO CONCURRENT construct
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all dc4.f90 -o dc4
$ ./dc4
the array: 0.1988 -0.0563 -0.0493 -0.0120 0.0019 0.0024
its sum: .085477
What gfortran does with the promise
The construct is a promise about independence, not a request for threads. A
compiler may run the iterations in any order on one processor and still be
correct, and gfortran 13 does exactly that. The flag
-ftree-parallelize-loops=n asks the middle end of the compiler to
split loops into n OpenMP threads, and the program below measures
whether that happened by timing five million iterations.
program dc5
use iso_fortran_env, only: real64, int64
implicit none
integer, parameter :: n = 5000000
real(real64) :: a(n), x
integer(int64) :: tick0, tick1, rate
integer :: i
call system_clock(tick0, rate)
do concurrent (i = 1:n)
x = real(i, real64) * 1.0000001_real64
a(i) = x * x + x
end do
call system_clock(tick1)
print '(a, f0.4, a)', 'time: ', real(tick1 - tick0, real64) / &
real(rate, real64), ' s'
print '(a, es14.6)', 'the total: ', sum(a)
end program dc5
The same source is compiled twice, once plain and once with the flag, and both binaries take the same time and print the same total. The last command searches the compiler's dump file for the message it writes when it splits a loop, and finds nothing, so the times are not a measurement error: no parallel region was created. Optimisation is on in both builds, since the flag only works on optimised code.
$ gfortran -O2 -std=f2018 -Wall -Wextra -fcheck=all dc5.f90 -o dc5 $ gfortran -O2 -std=f2018 -Wall -Wextra -fcheck=all -ftree-parallelize-loops=4 -fdump-tree-parloops-details dc5.f90 -o dc5p $ ./dc5 time: .0123 s the total: 4.166670E+19 $ ./dc5p time: .0126 s the total: 4.166670E+19 $ grep -H "parallelizing" *dc5.f90.*.parloops* 2>/dev/null || echo "no parallel loop found" no parallel loop found
An ordinary loop that the flag does split
The same body in a plain do loop is a different story. The
parallelizer accepts loops whose iterations are simple and whose bounds it can
prove, and five million assignments with a little arithmetic inside qualify.
The program is the one above with concurrent removed, and the dump
file now contains the line that reports the split.
program dc6
use iso_fortran_env, only: real64, int64
implicit none
integer, parameter :: n = 5000000
real(real64) :: a(n), x
integer(int64) :: tick0, tick1, rate
integer :: i
call system_clock(tick0, rate)
do i = 1, n
x = real(i, real64) * 1.0000001_real64
a(i) = x * x + x
end do
call system_clock(tick1)
print '(a, f0.4, a)', 'time: ', real(tick1 - tick0, real64) / &
real(rate, real64), ' s'
print '(a, es14.6)', 'the total: ', sum(a)
end program dc6
The flagged binary finishes in roughly a third of the time of the plain one,
and both totals are identical, since the iterations were independent in the
first place. The grep line names the source line of the loop and the phrase
optimized: parallelizing inner loop 1, which is the compiler
stating what it did. This is the reason -ftree-parallelize-loops
exists at all: it can turn a loop that already looks parallel into a parallel
region without a single directive in the source.
$ gfortran -O2 -std=f2018 -Wall -Wextra -fcheck=all dc6.f90 -o dc6 $ gfortran -O2 -std=f2018 -Wall -Wextra -fcheck=all -ftree-parallelize-loops=4 -fdump-tree-parloops-details dc6.f90 -o dc6p $ ./dc6 time: .0110 s the total: 4.166670E+19 $ ./dc6p time: .0039 s the total: 4.166670E+19 $ grep -H "parallelizing" *dc6.f90.*.parloops* 2>/dev/null || echo "no parallel loop found" dc6p-dc6.f90.168t.parloops2:dc6.f90:10:10: optimized: parallelizing inner loop 1
Where the parallelizer gives up
A call to an intrinsic such as sqrt is enough to stop the
transformation. The compiler cannot see inside the call, cannot prove that it
has no side effects, and therefore leaves the loop alone; the same happens with
a call to a function of your own, with a loop that writes to a variable shared
between iterations, and with a loop whose trip count is only known at run time.
The last program puts twenty million square roots in a loop and compiles it both
ways.
program dc7
use iso_fortran_env, only: real64, int64
implicit none
integer, parameter :: n = 20000000
real(real64) :: a(n)
integer(int64) :: tick0, tick1, rate
integer :: i
call system_clock(tick0, rate)
do i = 1, n
a(i) = sqrt(real(i, real64))
end do
call system_clock(tick1)
print '(a, f0.4, a)', 'time: ', real(tick1 - tick0, real64) / &
real(rate, real64), ' s'
print '(a, es14.6)', 'the total: ', sum(a)
end program dc7
The two times are the same within the noise, and the dump search again reports
no parallel loop, so the flag changed nothing. For loops of this shape the
directives of OpenMP, or the coarray features of Fortran, are the tools that
work, and both are the subject of the next two articles. The lesson of the three
timing programs is that do concurrent documents an intention and
lets a compiler exploit it, while the flags of a particular compiler decide
whether anything happens.
$ gfortran -O2 -std=f2018 -Wall -Wextra -fcheck=all dc7.f90 -o dc7 $ gfortran -O2 -std=f2018 -Wall -Wextra -fcheck=all -ftree-parallelize-loops=4 -fdump-tree-parloops-details dc7.f90 -o dc7p $ ./dc7 time: .0678 s the total: 5.962848E+10 $ ./dc7p time: .0676 s the total: 5.962848E+10 $ grep -H "parallelizing" *dc7.f90.*.parloops* 2>/dev/null || echo "no parallel loop found" no parallel loop found
Next steps
The OpenMP article adds directives that ask for
threads directly, and the coarrays article
spreads the work over several images of the program. The
loops article explains the counted
do that do concurrent extends, the
arrays article covers the element and section
syntax used by every iteration, and the
procedure arguments article
describes the pure procedures that the construct requires. The
error handling article explains what a
failed compiler run and a nonzero exit status mean.
Source
Fortran 2018 standard (J3/18-007r1)
GNU Fortran manual: Optimize Options
GCC wiki: DO CONCURRENT support
In this article we have written loops with do concurrent, filled an
array and a two-dimensional table, smoothed a row with two arrays and checked
the result against a serial loop, seen the accumulator mistake that the
construct cannot catch, watched the compiler reject an impure function and an
early exit, and measured what
-ftree-parallelize-loops does: nothing to
do concurrent, a real parallel region for a plain loop with a
simple body, and nothing for a loop that calls sqrt. The previous
article, Fortran scientific
simulations, contains loops of exactly the kind these constructs are meant
for.
Author
List all Fortran tutorials.