ZetCode

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.

dc1.f90
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.

dc2.f90
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.

dc3.f90
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.

bad_pure.f90
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.

bad_exit.f90
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.

dc4.f90
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.

dc5.f90
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.

dc6.f90
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.

dc7.f90
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Fortran tutorials.