>
>
OpenMP and static code analysis

Andrey Karpov
Articles: 673

Evgenii Ryzhkov
Articles: 125

OpenMP and static code analysis

The article describes principles on which implementation of the static code analyzer VivaMP is based. The described set of testing logical conditions allows you to diagnose some errors in parallel programs created on the basis of OpenMP technology.

VivaMP support had been canceled in the year 2014. If you have any questions, feel free to contact our support.

Introduction

Many errors in the programs developed on the basis of OpenMP technology can be diagnosed with the help of the static code analyzer [1]. This article gives a set of diagnostic rules detecting potentially unsafe code sections which most likely contain errors. The rules described below are meant for testing C and C++ code. But many rules can be applied to the Fortran programs as well after some modification.

The rules described in the article are the basis of the static code analyzer VivaMP developed by OOO "Program Verification Systems" [2]. VivaMP analyzer is intended for testing code of C/C++ applications. The analyzer integrates into Visual Studio 2005/2008 development environments and also adds a documentation section into MSDN Help system. VivaMP code analyzer is included in PVS-Studio software product.

This article is being renewed and modified together with the development of PVS-Studio. At present, this is the third variant of the article, which refers to PVS-Studio 3.40. If it is mentioned in the article that some diagnostic features are not implemented, this refers to PVS-Studio 3.40. In the following versions of the analyzer, such features can be implemented. See a newer variant of this article or PVS-Studio documentation.

Diagnostic rules

It is considered that the directives in all the rules are used together with "omp" directive, if not stated otherwise. That is, we omit mentioning that "omp" directive is used in order to shorten the rules' text.

Generic exception A

This exception is used in several rules and is singled out to shorten their description. In general, the point is that we operate outside a parallel section or explicitly point which thread is used or shield the code with lockups.

We can consider a case safe when one of the following conditions is satisfied for the code being tested:

  • The parallel section is absent (there is no "parallel" directive).
  • A critical section defined by "critical" directive is used inside the parallel section.
  • There is a "master"-block inside the parallel section.
  • There is a "single"-block inside the parallel section.
  • There is an "ordered"-block inside the parallel section.
  • Inside the parallel section the omp_set_lock function is used and blocking is set.

Rule N1

We should consider unsafe using "for" and "sections" directives without "parallel" directive.

Exceptions:

"for" or "sections" directives are located inside the parallel section defined by "parallel" directive.

An example of unsafe code:

#pragma omp for
for(int i = 0; i < 100; i++)
  ...

An example of safe code:

#pragma omp parallel
{
  #pragma omp for
  for(int i = 0; i < 100; i++)
    ...
}

Diagnostic messages based on this rule:

V1001. Missing 'parallel' keyword.

Rule N2

We should consider unsafe using one of the directives relating to OpenMP without "omp" directive.

Exceptions:

Using "warning" directive.

An example of unsafe code:

#pragma single

An example of safe code:

#pragma warning(disable : 4793)

Diagnostic messages based on this rule:

V1002. Missing 'omp' keyword.

Rule N3

We should consider unsafe using for operator immediately after "parallel" directive without "for" directive.

An example of unsafe code:

#pragma omp parallel num_threads(2)
for(int i = 0; i < 2; i++)
  ...

An example of safe code:

#pragma omp parallel num_threads(2)
{
  for(int i = 0; i < 2; i++)
    ...
}

Diagnostic messages based on this rule:

V1003. Missing 'for' keyword. Each thread will execute the entire loop.

Rule N4

We should consider unsafe creating a parallel loop with "parallel" and "for" directives inside a parallel section created by "parallel" directive.

An example of unsafe code:

#pragma omp parallel
{  
  #pragma omp parallel for
  for(int i = 0; i < 100; i++)
    ...
}

An example of safe code:

#pragma omp parallel
{  
  #pragma omp for
  for(int i = 0; i < 100; i++)
    ...
}

Diagnostic messages based on this rule:

V1004. Nested parallelization of a 'for' loop.

Rule N5

We should consider unsafe using "for" and "ordered" directives together if after that "ordered" directive is not used inside the loop defined by for operator.

An example of unsafe code:

#pragma omp parallel for ordered
for(int i = 0; i < 4; i++)
{
  foo(i);
}

An example of safe code:

#pragma omp parallel for ordered
for(int i = 0; i < 4; i++)
{
    #pragma omp ordered
    {
    foo(i);
  }
}

Diagnostic messages based on this rule:

V1005. The 'ordered' directive is not present in an ordered loop.

Rule N6

We should consider unsafe call of omp_set_num_threads function inside a parallel section defined by "parallel" directive.

An example of unsafe code:

#pragma omp parallel
{
  omp_set_num_threads(2);
}

An example of safe code:

omp_set_num_threads(2);
#pragma omp parallel
{
  ...
}

Diagnostic messages based on this rule:

V1101. Redefining number of threads in a parallel code.

Rule N7

We should consider unsafe odd use of omp_set_lock, omp_set_nest_lock, omp_unset_lock and omp_unset_nest_lock functions inside a parallel section.

An example of unsafe code:

#pragma omp parallel sections
{
  #pragma omp section
  {
    omp_set_lock(&myLock);
  }
}

An example of safe code:

#pragma omp parallel sections
{
  #pragma omp section
  {
    omp_set_lock(&myLock);
    omp_unset_lock(&myLock);
  }
}

Diagnostic messages based on this rule:

V1102. Non-symmetrical use of set/unset functions for the following lock variable(s): %1%.

Rule N8

We should consider unsafe using omp_get_num_threads function in arithmetic operations.

Exceptions:

The returned value of omp_get_num_threads function is used for comparing or equating with a variable.

An example of unsafe code:

int lettersPerThread =
  26 / omp_get_num_threads();

An example of safe code:

bool b = omp_get_num_threads() == 2;
switch(omp_get_num_threads())
{
  ...
}

Diagnostic messages based on this rule:

V1103. Threads number dependent code. The 'omp_get_num_threads' function is used in an arithmetic expresion.

Rule N9

We should consider unsafe call of omp_set_nested function inside a parallel section defined by "parallel" directive.

Exceptions:

The function is located in an embedded block created by "master" or "single" directive.

An example of unsafe code:

#pragma omp parallel
{
  omp_set_nested(2);
}

An example of safe code:

#pragma omp parallel
{
  #pragma omp master
  {
    omp_set_nested(2);
  }
}

Diagnostic messages based on this rule:

V1104. Redefining nested parallelism in a parallel code.

Rule N10

We should consider unsafe functions using common resources. An example of such functions is printf.

Exceptions:

Generic exception A.

An example of unsafe code:

#pragma omp parallel
{
  printf("abcd");
}

An example of safe code:

#pragma omp parallel
{
  #pragma omp critical
  {
    printf("abcd");
  }
}

Diagnostic messages based on this rule:

V1201. Concurrent usage of a shared resource via an unprotected call of the '%1%' function.

Rule N11

We should consider unsafe applying flush directive to pointers.

An example of unsafe code:

int *t;
...
#pragma omp flush(t)

An example of safe code:

int t;
...
#pragma omp flush(t)

Diagnostic messages based on this rule:

V1202. The 'flush' directive should not be used for the '%1%' variable, because the variable has pointer type.

Rule N12

We should consider unsafe using "threadprivate" directive.

An example of unsafe code:

#pragma omp threadprivate(var)

Diagnostic messages based on this rule:

V1203. Using the 'threadprivate' directive is dangerous, because it affects the entire file. Use local variables or specify access type for each parallel block explicitly instead.

Rule N13

We should consider unsafe initialization or modification of an object (variable) in a parallel section if the object is global (common for the threads) in relation to this section.

Explanation of the rule:

The following objects are global in relation to a parallel section:

  • Static variables.
  • Static class members (in the current version of VivaMP, this rule is not implemented).
  • Variables defined outside the parallel section.
  • During the analysis of the code of the function which is called in a parallel way, global variables are considered global objects. If the analyzed function is a class member, then to consider the class members global or not depends on the way the call of this function is carried out. If the call is carried out from another function of the given class, the members of the class are considered global. If the call is carried out with the help of operator '.' or '->', the objects are also considered global.
  • The last item needs explaining. Let us site an example:

class MyClass {

public:

int m_a;

void IncFoo() { a++; }

void Foo() {

#pragma omp parallel for

for (int i = 0; i < 10; i++)

IncFoo(); // Variant. A

}

};

MyClass object_1;

#pragma omp parallel for

for (int i = 0; i < 10; i++)

{

object_1.IncFoo(); // Variant. B

MyClass object_2;

object_2.IncFoo(); // Variant. C

  • }
  • In the case of variant A, we will consider the class members common, i.e. they are global in reference to function IncFoo. As a result, we will detect an error of race condition inside function IncFoo.
  • In case of variant B, we will consider the class members local, and there is no error in IncFoo. However, there will be given a warning that a nonstick method of IncFoo is called in a parallel way from MyClass class. This will allow to find the error.
  • In the case of variant C, we will consider that the class members are local and that there is no error in IncFoo. And there are really no errors.

The object can be both of simple type and direct instance. The following operations relate to operations of object change:

  • Transfer of an object into a function by the non-constant link.
  • Transfer of an object into a function by the non-constant pointer (In the current version of VivaMP, this rule is not implemented).
  • Change of an object during arithmetic operations or assignment operations.
  • Call of a non-constant method in the object.

Excpetions:

  • Generic exception A.
  • "threadprivate", "private", "firstprivate", "lastprivate" or "reduction" directives are applied to the object. This exception doesn't concern static variables and static class fields which are always generic.
  • Modification of an object is protected by "atomic" directive.
  • Modification of an object is performed inside of only one section defined by "section" directive.
  • Initialization or modification of objects is implemented inside parallelized for operator (inside the operator itself and not inside the loop's body). Such objects are automatically considered private according to OpenMP specification. An example:
  • int i;
  • ...
  • #pragma omp parallel for
  • for (i = 0; i < n; i++) {}. // i - is private.

An example of unsafe code:

#pragma omp parallel
{
  static int st = 1; // V1204
}
void foo(int &) {}
...
int value;
MyObjectType obj;
#pragma omp parallel for
for(int i = 0; i < 33; i++)
{
  ++value; // V1205
  foo(value); // V1206
  obj.non_const_foo(); // V1207
}

An example of safe code:

#pragma omp parallel
{
  #pragma omp critical
  {
    static int st = 1;
  }
}
void foo(const int &) {}
...
int value;
MyObjectType obj;
#pragma omp parallel for
for(int i = 0; i < 33; i++)
{
  #pragma omp atomic
  ++value;
  foo(value);
  obj.const_foo();
}

Diagnostic messages based on this rule:

V1204. Data race risk. Unprotected static variable declaration in a parallel code.

V1205. Data race risk. Unprotected concurrent operation with the '%1%' variable.

V1206. Data race risk. The value of the '%1%' variable can be changed concurrently via the '%2%' function.

V1207. Data race risk. The '%1%' object can be changed concurrently by a non-const function.

Rule N14

We should consider unsafe applying "private", "firstprivate" and "threadprivate" directives to links and pointers (not arrays).

An example of unsafe code:

int *arr;
#pragma omp parallel for private(arr)

An example of safe code:

int arr[4];
#pragma omp parallel for private(arr)

Diagnostic messages based on this rule:

V1208. The '%1%' variable of reference type cannot be private.

V1209. Warning: The '%1%' variable of pointer type should not be private.

Rule N15

We should consider unsafe absence of modification of a variable marked by "lastprivate" directive in the last section ("section ").

Exceptions:

The variable is not modified in all the other sections as well.

An example of unsafe code:

#pragma omp sections lastprivate(a)
{
  #pragma omp section
  {
    a = 10;
  }
  #pragma omp section
  {
  }
}

An example of safe code:

#pragma omp sections lastprivate(a)
{
  #pragma omp section
  {
    a = 10;
  }
  #pragma omp section
  {
   a = 20;
  }
}

Diagnostic messages based on this rule:

V1210. The '%1%' variable is marked as lastprivate but is not changed in the last section.

Rule N16

We should consider unsafe using a variable of omp_lock_t / omp_nest_lock_t type without its being initialized beforehand inside omp_init_lock / omp_init_nest_lock function.

Under using we understand call of omp_set_lock function etc.

An example of unsafe code:

omp_lock_t myLock;
#pragma omp parallel num_threads(2)
{
  ...
  omp_set_lock(&myLock);
}

An example of safe code:

omp_lock_t myLock;
omp_init_lock(&myLock);
#pragma omp parallel num_threads(2)
{
  ...
  omp_set_lock(&myLock);
}

Diagnostic messages based on this rule:

In the current version of VivaMP, this rule is not implemented.

Rule N17

We should consider unsafe using variables defined in a parallel section as private with use of "private" and "lastprivate" without their being initialized beforehand.

An example of unsafe code:

int a = 0;
#pragma omp parallel private(a)
{
  a++;
}

An example of safe code:

int a = 0;
#pragma omp parallel private(a)
{
  a = 0;
  a++;
}

Diagnostic messages based on this rule:

In the current version of VivaMP, this rule is not implemented.

Rule N18

We should consider unsafe the case when after a parallel section variables are used to which "private", "threadprivate" or "firstprivate" directive was applied without its being initialized beforehand.

An example of unsafe code:

#pragma omp parallel private(a)
{
  ...
}
a++;

An example of safe code:

#pragma omp parallel private(a)
{
  ...
}
a = 10;

Diagnostic messages based on this rule:

In the current version of VivaMP, this rule is not implemented.

Rule N19

We should consider unsafe applying "firstprivate" and "lastprivate" directives to direct instances in which copy constructor is absent.

Diagnostic messages based on this rule:

In the current version of VivaMP, this rule is not implemented.

Rule N20

We should consider ineffective using "flush" directive where it is executed implicitly. The cases where "flush" directive is implicit and there is no sense in using it:

  • In Barrier directive
  • When critical directive enters and leaves the parallel section
  • When ordered directive enters and leaves the parallel section
  • When parallel directive enters and leaves the parallel section
  • When for directive leaves the parallel section
  • When sections directive leaves the parallel section
  • When single directive leaves the parallel section
  • When parallel for directive enters and leaves the parallel section
  • When parallel sections directive enters and leaves the parallel section

Diagnostic messages based on this rule:

In the current version of VivaMP, this rule is not implemented.

Rule N21

We should consider ineffective the use of flush directive for local variables (declared in the parallel section), as well as of variables marked as threadprivate, private, lastprivate, and firstprivate.

Example:

int a = 1;
#pragma omp parallel for private(a)
for (int i = 10; i < 100; ++i) {
  #pragma omp flush(a);
  ...

}

Diagnostic messages based on this rule:

V1211. The use of 'flush' directive has no sense for private 'NN' variable, and can reduce performance.

Rule N22

We should consider ineffective using critical sections or functions of omp_set_lock class where "atomic" directive is enough.

Diagnostic messages based on this rule:

In the current version of VivaMP, this rule is not implemented.

Rule N23

We should consider ineffective using flush directive for local variables (declared in a parallel section) and also for variables marked as threadprivate, private, lastprivate, firstprivate.

flush directive has no sense for the enumerated variables because they always contain actual values. But it reduces the code performance.

An example of unsafe code:

int a = 1;
#pragma omp parallel for private(a)
for (int i = 10; i < 100; ++i) {
  #pragma omp flush(a);
  ...
}

An example of safe code:

int a = 1;
#pragma omp parallel for
for (int i = 10; i < 100; ++i) {
  #pragma omp flush(a);
  ...
}

Diagnostic warnings based on this rule:

In the current version of VivaMP, this rule is not implemented.

Rule N24

According to OpenMP specification, all the exceptions must be processed inside a parallel section. It is considered that the code generates exceptions if:

throw operator is used in it;

new operator is used in it;

a function marked as throw(...) is called in it;

Such code must be inside a try..catch block inside a parallel section.

Exceptions:

new operator is used that does not throw exceptions (new(std::nothrow) float[10000];).

An example of unsafe code:

void MyNotThrowFoo() throw() { }
...
#pragma omp parallel for num_threads(4)
for(int i = 0; i < 4; i++)
{
  ...
  throw 1;
  ...
  float *ptr = new float[10000];
  ...
  MyThrowFoo();
}

An example of safe code:

size_t errCount = 0;
#pragma omp parallel for num_threads(4) reduction(+: errCount)
for(int i = 0; i < 4; i++)
{
  try {
    //...
    throw 1;
  }
  catch (...)
  {
    ++errCount;
  }
}
if (errCount != 0)
  throw 1;

Note: nothrow new construction is somewhat deceptive, as there can occur an impression that there can be no exceptions here. But we should take into account that exceptions can be generated in the builder of the created objects. That means, if at least one std::string is allocated or a class itself allocates memory by new (without nothrow), then exceptions at call of new(nothrow) anyway can be generated. The diagnostics of the given errors lies in the analysis of builders bodies (and other objects builders bodies included in the class), which are called inside parallel sections. At present, this feature is not implemented in VivaMP.

Diagnostic messages based on this rule:

V1301. The 'throw' keyword cannot be used outside of a try..catch block in a parallel section.

V1302. The 'new' operator cannot be used outside of a try..catch block in a parallel section.

V1303. The '%1%' function which throws an exception cannot be used in a parallel section outside of a try..catch block.

Rule N25

We should consider unsafe not including the header file <omp.h> in the file where OpenMP directives are used.

Diagnostic messages based on this rule:

V1006. Missing omp.h header file. Use '#include <omp.h>'.

Rule N26

We should consider unsafe the presence of unused variables marked in the reduction directive. This may be evidence both of an error presence or simply that some other directive or variable was forgotten and not removed during the process of code refactoring.

Example of not using abcde variable:

#pragma omp parallel for reduction (+:sum, abcde)
for (i=1; i<999; i++)
{ 
  sum = sum + a[i];

Diagnostic messages based on this rule:

In the current version of VivaMP, this rule is not implemented.

Rule N27

You should consider unsafe an unprotected access in a parallel loop to an array item using an index different from that used for reading.

Note. The error might occur in case of a protected access as well (single, critical, ...), but for the purpose of clearness we will consider here that the protected access is safe in this case.

Exceptions:

1. The index is a constant.

Here is an example of dangerous code:

#pragma omp parallel for
  for (int i=2; i < 10; i++) 
     array[i] = i * array[i-1]; //V1212

This is an example of safe code:

#pragma omp parallel for
  for (int i=2; i < 10; i++)
  {
    array[i] = array [i] / 2;
    array_2[i] = i * array[i-1];
  }

Diagnostic warnings that are generated relying on this rule:

V1212. Data race risk. When accessing the array '%1%' in a parallel loop, different indexes are used for writing and reading.

Conclusion

If you are interested in methodology of testing program code on the basis of static analysis, write us (support@viva64.com). We hope that we will find mutual interests and opportunities to collaborate!

References