Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Programming with OpenMP

OpenMP is the simplest way to parallelize an existing code on a single node. By adding a few compiler directives to a serial program, you can distribute the work over all the cores of a shared memory machine, without rewriting your algorithms.

In this lecture, we will:

Parallel programming languages

Evolution of programming methods:

Distributed memory versus shared memory

OpenMP history

Multi-threading using directives

During the era of vector processing (mid-90s), many vendors (Cray, NEC, IBM...) were using their own multi-threading directives. On October 28th 1997, they all met and adopted the Open Multi-Processing standard. OpenMP is specified by the Architecture Review Board (ARB).

General concepts

Multi-threading

Compilation

An OpenMP program relies on three ingredients:

Example:

$ gfortran -fopenmp prog.f90
$ ifx -qopenmp prog.f90
$ export OMP_NUM_THREADS=4

Parallel region

Shared variables

program parallel
  !$ use OMP_LIB
  implicit none
  real    :: a
  logical :: p

  a = 92290. ; p=.false.
  !$OMP PARALLEL
  !$ p = OMP_IN_PARALLEL()
  print *,"A = ",a, &
          "; p = ",p
  !$OMP END PARALLEL
end program parallel
$ gfortran -fopenmp prog.f90
$ export OMP_NUM_THREADS=4
$ ./a.out
 A =    92290.0000     ; p =  T
 A =    92290.0000     ; p =  T
 A =    92290.0000     ; p =  T
 A =    92290.0000     ; p =  T

Lines starting with !$ are only compiled when OpenMP is enabled. This is called conditional compilation: without -fopenmp, the code above is still a valid serial program.

Private variables

program parallel
  implicit none
  real :: a

  a = 92000.
  !$OMP PARALLEL DEFAULT(PRIVATE)
    a = a + 290.
    print *,"A = ",a
  !$OMP END PARALLEL
end program parallel
$ gfortran -fopenmp prog.f90
$ export OMP_NUM_THREADS=4
$ ./a.out
 A =    290.000000
 A =    290.000000
 A =    290.000000
 A =               NaN

The value 92000 set before the parallel region is lost: each thread starts from an uninitialized copy of a, which here happened to contain 0 or NaN.

program parallel
  implicit none
  real :: a

  a = 92000.
  !$OMP PARALLEL DEFAULT(NONE) &
  !$OMP FIRSTPRIVATE(a)
    a = a + 290.
    print *,"A = ",a
  !$OMP END PARALLEL
  print*,"Out of region, A =",a
end program parallel
$ gfortran -fopenmp prog.f90
$ export OMP_NUM_THREADS=4
$ ./a.out
 A =    92290.0000
 A =    92290.0000
 A =    92290.0000
 A =    92290.0000
 Out of region, A =   92000.0000

Number of threads

program parallel
  implicit none

  !$OMP PARALLEL NUM_THREADS(2)
    print *,"Hello !"
  !$OMP END PARALLEL

  !$OMP PARALLEL NUM_THREADS(3)
    print *,"Hi !"
  !$OMP END PARALLEL
end program parallel
$ gfortran -fopenmp prog.f90
$ export OMP_NUM_THREADS=4
$ ./a.out
 Hello !
 Hello !
 Hi !
 Hi !
 Hi !

Parallel loop

Work decomposition

Work scheduling

Examples

With the default scheduling, the n=4096n = 4096 iterations are divided into 4 contiguous chunks of 1024 iterations, one per thread:

program parallel
  !$ use OMP_LIB
  implicit none
  integer, parameter :: n=4096
  real,dimension(n) :: a
  integer :: i, rank, nb_threads

  !$OMP PARALLEL PRIVATE(rank,nb_threads)
  rank=OMP_GET_THREAD_NUM()
  nb_threads=OMP_GET_NUM_THREADS()
  !$OMP DO
  do i=1,n
     print*,rank,i
     a(i) = 92290. + real(i)
  end do
  !$OMP END DO
  !$OMP END PARALLEL

end program parallel
$ gfortran -fopenmp prog.f90
$ export OMP_NUM_THREADS=4
$ ./a.out | more
           1        1025
           0           1
           3        3073
           2        2049
           1        1026
           0           2
           3        3074
           2        2050
...

With a STATIC schedule and a chunk size of 128, the iterations are distributed in a round-robin fashion by blocks of 128:

  !$OMP DO SCHEDULE(STATIC,128)
  do i=1,n
     print*,rank,i
     a(i) = 92290. + real(i)
  end do
  !$OMP END DO
$ ./a.out | more
           2         257
           0           1
           1         129
           3         385
           2         258
           0           2
           1         130
           3         386
...

With a RUNTIME schedule, the strategy is chosen at execution time with the OMP_SCHEDULE environment variable. Here, a GUIDED schedule starts with large chunks and decreases their size down to a minimum of 16:

  !$OMP DO SCHEDULE(RUNTIME)
  do i=1,n
     print*,rank,i
     a(i) = 92290. + real(i)
  end do
  !$OMP END DO
$ export OMP_SCHEDULE="GUIDED,16"
$ ./a.out | more
           0           1
           2        1025
           3        1793
           1        2369
           0           2
           2        1026
           3        1794
           1        2370
...

Synchronization and reductions

Reductions

program parallel
  implicit none
  integer, parameter :: n=5
  integer            :: i, s=0, p=1, r=1
  !$OMP PARALLEL
  !$OMP DO REDUCTION(+:s) REDUCTION(*:p,r)
    do i = 1, n
      s = s + 1
      p = p * 2
      r = r * 3
    end do
  !$OMP END PARALLEL
  print *,"s =",s, "; p =",p, "; r =",r
end program parallel

Barriers and race conditions

Best practices

Memory placement