OpenMP logo

This article discusses recent features implemented in the GCC compiler version 12, the latest stable release. The new features were implemented as a joint effort between Red Hat and CodeSourcery, now a part of Siemens EDA.

OpenMP is an API consisting of compiler directives and library routines that implement high-level parallelism in C and C++ as well as Fortran. OpenMP version 5.1 was released in November 2020, and version 5.2 was released in November 2021. Support for various OpenMP 5.1 features has been added. But GCC 12 does not yet have complete support for OpenMP 5.0, and work is ongoing in GCC 13.

C++11 attribute syntax

Traditionally, the OpenMP directives were expressed in C++ using a pragma syntax, #pragma omp ... or _Pragma("omp ..."). Now the directives can use the C++11 omp::directive and omp::sequence attributes. This new feature was mentioned in a previous article. The support is implemented in GCC 12.

Neither OpenMP 5.1 nor OpenMP 5.2 specify in detail where those attributes can appear, so different compilers make different choices. Hopefully, the positioning will be clarified in OpenMP 6.0. The standard does not allow attribute and pragma syntaxes to be mixed in the same statement, but different statements in the same program can use different syntax.

Standalone directives are executable directives that don't have associated base language statements. GCC enforces a rule that standalone directives can appear only on empty statements and that each standalone directive must have its own empty statement. For instance, the following directive is allowed in GCC:

[[omp::directive (barrier)]];

But the following directive is not allowed because it includes a C++ statement with the directive:

[[omp::directive (barrier)]] ++i;

Other executable directives should be specified on the C++ statement that forms their body. To put more than one directive in a statement, use the omp::sequence attribute, such as:

[[omp::sequence (directive (parallel), directive (masked)]] { // body }

At namespace scope, most directives that are allowed should be specified in attribute declarations, followed immediately by a semicolon as follows:

[[omp::directive (requires, atomic_default_mem_order (seq_cst))]];

The declare simd and declare variant directives need a function declaration or definition. One option is to specify the directives in attributes at the start of the declaration, like:

[[omp::directive (declare simd, linear (l))]] extern int f1 (int l);

But similar to the pragma syntax, only one function can be declared in the declaration.

Another option is to specify the attributes immediately after the function name. Using this syntax, the statement can contain other declarations:

extern int f2 (int), f3 [[omp::directive (declare simd, uniform (m))]] (int m), z;

Some directives, such as threadprivate and allocate, bind to variable declarations. But unfortunately, they require the directive to specify the variable's name. The location of the attributes relative to the variable name isn't in scope for C++ yet. In short, these directives need to be specified on an empty statement or alias declaration.

Perhaps OpenMP 6.0 will define a new syntax for those directives that allows the variable name to be omitted when using the C++ attribute syntax on a variable declaration.

Atomic directive extensions in OpenMP 5.1

In OpenMP 5.0 and earlier, atomic directives could perform atomic loads, stores, exchanges, fetch and op operations, and op and fetch operations. But there was no syntax to do atomic compare and swap or C/C++ atomic min or max (which was possible in Fortran before).

To support these operations, two new clauses have been introduced for atomic directives: compare and fail. The capture clause can be used with the update clause. And when a compare clause is present, the standard allows the new atomic operations to be expressed with different syntax types.

Atomic min and max in C and C++ can be written using the ?: ternary operator like this:

#pragma omp atomic compare
x = x > 8 ? 8 : x;

Alternatively, the program can use an if statement:

#pragma omp atomic update, compare
if (x > 8) { x = 8; }

Both constructs are functionally equivalent to the following function in OpenMP 3.0 Fortran:

!$omp atomic
x = min (x, 8)

If the value of x (an atomically updated memory location) is needed before the min or max operation, one can add the capture clause and write it like this:

#pragma omp atomic compare update capture
{ v = x; x = x > 8 ? 8 : x; }

Using an if statement, the directive would be:

#pragma omp atomic capture compare
{ v = x; if (x > 8) { x = 8; } }

These constructs are functionally equivalent to the following in OpenMP 3.5 Fortran:

!$omp atomic capture
v = x
x = min (8, x)
!$omp end atomic

If the value of x is needed after the operation, one can use (in the following example of a max operation):

#pragma omp atomic compare, capture
v = x = x < 42 ? 42 : x;

You can add an update clause:

#pragma omp atomic capture, update, compare
{ x = x > 42 ? x : 42; v = x; }

Using an if statement, the directive would be:

#pragma omp atomic capture compare
{ if (x < 42) { x = 42; } v = x; }

These constructs are functionally equivalent to the following in OpenMP 3.5 Fortran:

!$omp atomic capture
x = max (x, 42)
v = x
!$omp end atomic

It is also possible to use the ?: ternary operator or an if statement to express atomic compare and swap in C and C++:

#pragma omp atomic compare
x = x == 42 ? 15 : x;

That syntax is equivalent to the following, except that the following syntax contains extra clauses. These clauses specify that results will be sequentially consistent on success (when x atomically compares equal to 42, it will be replaced with 15) and specify acquire semantics on failure:

#pragma omp atomic compare update seq_cst fail(acquire)
if (x == 42) { x = 15; }

Fortran accepts only an if statement for atomic compare and swap. The operation would be written in Fortran as:

!$omp atomic compare
if (x == 42) x = 15

An alternative Fortran syntax is:

!$omp atomic compare
if (x == 42) then
x = 15
end if

If capture is needed too, one can use the previous forms with a preceding or following v = x statement. Curly braces must be used to surround the operation:

#pragma omp atomic capture compare
{ v = x; x = x == y[31] + 15 ? y[31] + 15 : x; }

When using the ternary ?: operator, the syntax can be:

#pragma omp atomic compare capture
v = x = x == 42 ? 15 : x;

To simply store a Boolean flag to indicate whether the compare and swap operation was successful, use:

#pragma omp atomic compare, capture
{ r = x == 42; if (r) { x = 15; } }

The result can be conditionally stored in v:

#pragma omp atomic capture, compare, update
if (x == 42) { x = 15; } else { v = x; }

A syntax using an if statement is:

#pragma omp atomic compare capture
{ r = x == 42; if (r) { x = 15; } else { v = x; }

As in older OpenMP versions, the x in these constructs can be a more complex lvalue expression, but it needs to be the same wherever it is used. The address of the expression and the tokens used in the source must be identical. Similar rules apply to other expressions.

Changes to C and C++ default clauses

In OpenMP 5.0 or older versions, the default clause could specify none, shared, private, or firstprivate in Fortran. But in C and C++  just none or shared were allowed. In 5.1, all four keywords can be specified in all three languages but with slightly different behavior in C and C++.

In C and C++, automatic variables will be privatized or firstprivatized by default, but variables with file or namespace scope static storage duration will act as default(none) with those keywords. This restriction was imposed because some heavily used macros from system headers could sometimes expand to such variables, and privatizing them could have undesirable effects.

The following example illustrates the results:

extern int x;

#define X x

void bar (int, int);

void foo () {
  int y = 6;

  #pragma omp parallel default(firstprivate)
  bar (X, y);
}

In this code, y is implicitly firstprivatized. Without the default clause, it would have been shared. However, the reference to x results in an error. To avoid the error, the code must explicitly declare shared(x) or firstprivate(x).

Structured block sequences

A structured block is roughly defined as a single (possibly compound) statement with certain requirements. Most importantly, the block must have single entry and exit points. The grammar of OpenMP 5.0 and older versions required that code between a #pragma omp sections directive and its body be a single structured block. The same requirement applied before and after a #pragma omp scan directive.

An example follows of an invalid structured block in these versions of OpenMP:

#pragma omp sections
{
  foo (1);
  bar (2);
  #pragma omp section
  baz (3);
  qux (4);
  #pragma omp section
  corge (5);
  garply (6);
}

Another invalid block is:

#pragma omp simd reduction (inscan, +: a)
for (i = 0; i < 64; i++) {
  int t = a;

  d[i] = t;
  #pragma omp scan exclusive (a)
  int u = c[i];
  a += u;
}

To make these examples valid, one would need to wrap the statements in curly braces.

OpenMP 5.1 validates such blocks by introducing a new term, structured block sequence. This stands for either a structured block or two or more statements that together satisfy the structured block requirements.

For Fortran, many OpenMP 5.0 constructs required a !$omp end directive to terminate the construct body. Such a directive was sometimes optional, primarily in looping constructs where the end is implied by the end of the loop that follows the directive.

OpenMP 5.1 makes the end directives optional in many other cases when the BLOCK keyword immediately follows the directive. After END BLOCK, the body implicitly or explicitly terminates.

The end directive is still required in an example such as:

!$omp parallel

call foo (1)

call bar (2)

!$omp end parallel ! Directive here is required

But the BLOCK and END BLOCK construct renders the end directive optional in the following:

!$omp parallel
block
  call baz (3)
  call qux (4)
end block
! !$omp end parallel is implied here, but could be specified explicitly.

In contrast, the following used to be valid in OpenMP 5.0 and earlier versions:

subroutine test

!$omp parallel
block
  call corge (5)
end block

call garply (6)
!$omp end parallel

end subroutine

The code isn't valid anymore. If the code would include another !$omp parallel directive after the subroutine test line, the end directive would match the outer parallel, not the inner one.

OpenMP 5.2 features

OpenMP 5.2, released just one year after 5.1, did not contain many new features. The main change is the use of various properties on directives or clauses to write the standard document. This effort has resulted in attempts to avoid using clauses with the same name but different meanings on different directives and to make the syntax more consistent.

To avoid using clauses with the same directive but different meanings, the declare target and update target now have different clauses that originally had the same name. The default clause on metadirective has also been changed to avoid confusion.

For example, the linear clause syntax has been changed in clause arguments and modifiers. This is an example of the old syntax:

linear (ref (x, y) : z)

The new syntax for this clause is:

linear (x, y : ref, step (z))

Many syntactically valid clauses in 5.1 are now deprecated, and support for them will be removed in OpenMP 6.0.

Find more resources

The OpenMP 5.1 and 5.2 standards, including HTML versions, are available on the OpenMP specifications site. The recently released GCC 12 supports the features described in this article and various others. But several other new OpenMP 5.0 features will be implemented in later GCC versions. The upcoming GCC 13 has already implemented a few OpenMP 5.2 features, and more is to come.

The GNU Offloading and Multi-Processing Project (GOMP) page summarizes various features in OpenMP 5.0, 5.1, and 5.2 implemented in GCC or waiting to be implemented. You can refer to those web pages to view the first version of GCC to implement each feature.

Please comment below with any questions or feedback. Stay tuned for more updates. In the meantime, check out the Red Hat Insights API Cheat Sheet, a helpful guide to addressing vulnerability risks in your Red Hat Enterprise Linux environments before an issue results in downtime.

Comments