The core elements of OpenMP are the constructs for thread creation, workload distribution (work sharing), data-environment management, thread synchronization, user-level runtime routines and environment variables. In C/C++, OpenMP uses
s. The OpenMP specific pragmas are listed below.
Thread creation The pragma omp parallel is used to fork additional threads to carry out the work enclosed in the construct in parallel. The original thread will be denoted as master thread with thread ID 0. Example (C program): Display "Hello, world." using multiple threads. • include • include int main(void) { #pragma omp parallel printf("Hello, world.\n"); return 0; } Use flag -fopenmp to compile using GCC: $ gcc -fopenmp hello.c -o hello -ldl Output on a computer with two cores, and thus two threads: Hello, world. Hello, world. However, the output may also be garbled because of the
race condition caused from the two threads sharing the
standard output. Hello, wHello, woorld. rld. Whether printf is atomic depends on the underlying implementation unlike C++11's std::cout, which is thread-safe by default.
Work-sharing constructs Used to specify how to assign independent work to one or all of the threads. • omp for or omp do: used to
split up loop iterations among the threads, also called loop constructs. • sections: assigning consecutive but independent code blocks to different threads • single: specifying a code block that is executed by only one thread, a barrier is implied in the end • master: similar to single, but the code block will be executed by the master thread only and no barrier implied in the end. Example: initialize the value of a large array in parallel, using each thread to do part of the work // define N as some constant for array length • define N 100000 int main(int argc, char* argv[]) { int a[N]; #pragma omp parallel for for (int i = 0; i This example is
embarrassingly parallel, and depends only on the value of i. The OpenMP parallel for flag tells the OpenMP system to split this task among its working threads. The threads will each receive a unique and private version of the variable. For instance, with two worker threads, one thread might be handed a version of i that runs from 0 to 49999 while the second gets a version running from 50000 to 99999.
Variant directives Variant directives are one of the major features introduced in OpenMP 5.0 specification to facilitate programmers to improve performance portability. They enable adaptation of OpenMP pragmas and user code at compile time. The specification defines traits to describe active OpenMP constructs, execution devices, and functionality provided by an implementation, context selectors based on the traits and user-defined conditions, and metadirective and declare directive directives for users to program the same code region with variant directives. • The metadirective is an executable directive that conditionally resolves to another directive at compile time by selecting from multiple directive variants based on traits that define an OpenMP condition or context. • The declare variant directive has similar functionality as metadirective but selects a function variant at the call-site based on context or user-defined conditions. The mechanism provided by the two variant directives for selecting variants is more convenient to use than the C/C++ preprocessing since it directly supports variant selection in OpenMP and allows an OpenMP compiler to analyze and determine the final directive from variants and context. // define array size as some constant for array length: • define N 100000 // code adaptation using preprocessing directives int v1[N]; int v2[N]; int v3[N]; • if defined(nvptx) • pragma omp target teams distribute parallel for map(to:v1,v2) map(from:v3) for (int i = 0; i
Clauses Since OpenMP is a shared memory programming model, most variables in OpenMP code are visible to all threads by default. But sometimes private variables are necessary to avoid
race conditions and there is a need to pass values between the sequential part and the parallel region (the code block executed in parallel), so data environment management is introduced as
data sharing attribute clauses by appending them to the OpenMP directive. The different types of clauses are:
Data sharing attribute clauses • shared: the data declared outside a parallel region is shared, which means visible and accessible by all threads simultaneously. By default, all variables in the work sharing region are shared except the loop iteration counter. • private: the data declared within a parallel region is private to each thread, which means each thread will have a local copy and use it as a temporary variable. A private variable is not initialized and the value is not maintained for use outside the parallel region. By default, the loop iteration counters in the OpenMP loop constructs are private. • default: allows the programmer to state that the default data scoping within a parallel region will be either shared, or none for C/C++, or shared, firstprivate, private, or none for Fortran. The none option forces the programmer to declare each variable in the parallel region using the data sharing attribute clauses. • firstprivate: the data is private to each thread, but initialized using the value of the variable using the same name from the master thread. • lastprivate: the data is private to each thread. The value of this private data will be copied to a global variable using the same name outside the parallel region if current iteration is the last iteration in the parallelized loop. A variable can be both firstprivate and lastprivate. • threadprivate: The data is a global data, but it is private in each parallel region during the runtime. The difference between threadprivate and private is the global scope associated with threadprivate and the preserved value across parallel regions.
Synchronization clauses • critical: the enclosed code block will be executed by only one thread at a time, and not simultaneously executed by multiple threads. It is often used to protect shared data from
race conditions. • atomic: the memory update (write, or read-modify-write) in the next instruction will be performed atomically. It does not make the entire statement atomic; only the memory update is atomic. A compiler might use special hardware instructions for better performance than when using critical. • ordered: the structured block is executed in the order in which iterations would be executed in a sequential loop • barrier: each thread waits until all of the other threads of a team have reached this point. A work-sharing construct has an implicit barrier synchronization at the end. • nowait: specifies that threads completing assigned work can proceed without waiting for all threads in the team to finish. In the absence of this clause, threads encounter a barrier synchronization at the end of the work sharing construct.
Scheduling clauses • schedule (type, chunk): This is useful if the work sharing construct is a do-loop or for-loop. The iterations in the work sharing construct are assigned to threads according to the scheduling method defined by this clause. The three types of scheduling are: • static: Here, all the threads are allocated iterations before they execute the loop iterations. The iterations are divided among threads equally by default. However, specifying an integer for the parameter chunk will allocate chunk number of contiguous iterations to a particular thread. • dynamic: Here, some of the iterations are allocated to a smaller number of threads. Once a particular thread finishes its allocated iteration, it returns to get another one from the iterations that are left. The parameter chunk defines the number of contiguous iterations that are allocated to a thread at a time. • guided: A large chunk of contiguous iterations are allocated to each thread dynamically (as above). The chunk size decreases exponentially with each successive allocation to a minimum size specified in the parameter chunk
IF control • if: This will cause the threads to parallelize the task only if a condition is met. Otherwise the code block executes serially.
Data copying • copyin: similar to firstprivate for private variables, threadprivate variables are not initialized, unless using copyin to pass the value from the corresponding global variables. No copyout is needed because the value of a threadprivate variable is maintained throughout the execution of the whole program. • copyprivate: used with single to support the copying of data values from private objects on one thread (the single thread) to the corresponding objects on other threads in the team.
Reduction • reduction (operator | intrinsic : list): the variable has a local copy in each thread, but the values of the local copies will be summarized (reduced) into a global shared variable. This is very useful if a particular operation (specified in operator for this particular clause) on a variable runs iteratively, so that its value at a particular iteration depends on its value at a prior iteration. The steps that lead up to the operational increment are parallelized, but the threads updates the global variable in a thread safe manner. This would be required in parallelizing
numerical integration of functions and
differential equations, as a common example.
Others • flush: The value of this variable is restored from the register to the memory for using this value outside of a parallel part • master: Executed only by the master thread (the thread which forked off all the others during the execution of the OpenMP directive). No implicit barrier; other team members (threads) not required to reach.
User-level runtime routines Used to modify/check the number of threads, detect if the execution context is in a parallel region, how many processors in current system, set/unset locks, timing functions, etc
Environment variables A method to alter the execution features of OpenMP applications. Used to control loop iterations scheduling, default number of threads, etc. For example, OMP_NUM_THREADS is used to specify number of threads for an application. == Implementations ==