I'm trying to understand whether the code below is OpenMP standard compliant. The main concern here is the args object that contains an offset field that is modified inside a loop to which #pragma omp simd is applied. Is this a legit use case?
#include <cstdio>
struct args_t {
int offset;
};
const int n = 10;
float data1[n];
float data2[n];
void foo(float &res, const args_t& args) {
res = res + data2[args.offset];
}
int main() {
printf("Original arrays:\n");
for (int i = 0; i < n; i++) {
data1[i] = (float)i / 2.0f;
printf("%f ", data1[i]);
}
printf("\n");
for (int i = 0; i < n; i++) {
data2[i] = (float)i / 3.0f;
printf("%f ", data2[i]);
}
printf("\n");
args_t args;
args.offset = 0;
#pragma omp simd
for (int i = 0; i < n; i++) {
foo(data1[i], args);
args.offset++;
}
printf("Sum of two arrays:\n");
for (int i = 0; i < n; i++)
printf("%f ", data1[i]);
printf("\n");
return 0;
}
TL;DR answer: OpenMP specification is not specific in this respect, which means that the answer depends on the actual implementation. In practice, your code properly vectorized (at least on newest gcc/clang on x86-64 platform), but you can specify that your variable is modified inside the loop by using the linear clause.
Detailed answer:
In the OpenMP specification the execution model of the simd construct is quite vaguely described:
The simd construct can be applied to a loop to indicate that the
loop can be transformed into a SIMD loop...
This gives a lot of flexibility/freedom to the compiler, and also raises many questions - like yours. The last paragraph of this document is much more clear:
OpenMP provides directives to improve the capabilities of the
compiler’s auto-vectorization pass by providing it with information
that cannot be determined through compile-time static-analysis. This
allows the programmer to effectively vectorize previously problematic
sections of code and have it run efficiently on several computer
architectures and accelerators...
This practically means that the OpenMP simd directives provide only information to the compiler for auto-vectorization, but how auto-vectorization is actually performed depends on the the implementation .
So, based on the above mentioned references and some tests with Compiler Explorer (gcc and clang on x86-64 platform) I always found that if you do not provide enough information for vectorization the worst case is that the loop won't be vectorized, but it will not result incorrect code.
I have also found that using #pragma omp simd without any additional clause or directive is practically equivalent to the use of #pragma GCC ivdep (or #pragma clang loop vectorize(assume_safety) for clang), but it is much more portable.
In the following code, the compiler generated code first checks the value of k to determine if it is safe to vectorize, but if #pragma omp simd is added this check is omitted:
void vec_dep(int *a, int k, int c, int m) {
for (int i = 0; i < m; i++)
a[i] = a[i + k] * c;
}
Consider the following example:
int foo(int* A){
int sum=0;
#pragma omp simd reduction(+:sum)
for(int i=0;i<1024;++i)
sum+=A[i];
return sum;
}
In this example #pragma omp simd reduction(+:sum) is the absolutely correct form, but using #pragma omp simd or #pragma GCC ivdep or not using anything at all gives similar (correctly vectorized) code. Note that this is not the case if #pragma omp parallel for reduction(+:sum) is used, in this case reduction is absolutely necessary to avoid race condition. (Well, this raises the obvious question why the compiler does not give a warning in such a case.)
Similarly it is not necessary to use linear clause (the compiler can find this linear dependence):
#pragma omp simd linear(b:1)
for (int i=0;i<N;++i) array[i]=b++;
Note that, however, if #pragma omp parallel for simd linear(b) is used the linear(b) cannot be omitted otherwise the result will be incorrect, because the OpenMP calculates the initial b value for each thread using this linear relationship.
So, to answer your question, your code will compile to properly vectorized code (at least on compilers I have tested), even though the linear relationship is not specified. To specify this linear relationship you have to use the linear clause. The first idea to use #pragma omp simd linear(args.offset), but it can't compile becasue the following error: linear clause applied to non-integral non-pointer variable with 'args_t' type. The workaround is to use a reference to args.offset and change the function foo accordingly:
void foo(float &res, const int& offset) {
res = res + data2[offset];
}
...
int& p=args.offset;
#pragma omp simd linear(p)
for (int i = 0; i < n; i++) {
foo(data1[i], p);
p++;
}
Related
I have the following piece of code.
for (i = 0; i < n; ++i) {
++cnt[offset[i]];
}
where offset is an array of size n containing values in the range [0, m) and cnt is an array of size m initialized to 0. I use OpenMP to parallelize it as follows.
#pragma omp parallel for shared(cnt, offset) private(i)
for (i = 0; i < n; ++i) {
++cnt[offset[i]];
}
According to the discussion in this post, if offset[i1] == offset[i2] for i1 != i2, the above piece of code may result in incorrect cnt. What can I do to avoid this?
This code:
#pragma omp parallel for shared(cnt, offset) private(i)
for (i = 0; i < n; ++i) {
++cnt[offset[i]];
}
contains a race-condition during the updates of the array cnt, to solve it you need to guarantee mutual exclusion of those updates. That can be achieved with (for instance) #pragma omp atomic update but as already pointed out in the comments:
However, this resolves just correctness and may be terribly
inefficient due to heavy cache contention and synchronization needs
(including false sharing). The only solution then is to have each
thread its private copy of cnt and reduce these copies at the end.
The alternative solution is to have a private array per thread, and at end of the parallel region you perform the manual reduction of all those arrays into one. An example of such approach can be found here.
Fortunately, with OpenMP 4.5 you can reduce arrays using a dedicate pragma, namely:
#pragma omp parallel for reduction(+:cnt)
You can have look at this example on how to apply that feature.
Worth mentioning that regarding the reduction of arrays versus the atomic approach as kindly point out by #Jérôme Richard:
Note that this is fast only if the array is not huge (the atomic based
solution could be faster in this specific case regarding the platform
and if the values are not conflicting). So that is m << n. –
As always profiling is the key!; Hence, you should test your code with aforementioned approaches to find out which one is the most efficient.
I have a nested loop, with few outer, and many inner iterations. In the inner loop, I need to calculate a sum, so I want to use an OpenMP reduction. The outer loop is on a container, so the reduction is supposed to happen on an element of that container.
Here's a minimal contrived example:
#include <omp.h>
#include <vector>
#include <iostream>
int main(){
constexpr int n { 128 };
std::vector<int> vec (4, 0);
for (unsigned int i {0}; i<vec.size(); ++i){
/* this does not work */
//#pragma omp parallel for reduction (+:vec[i])
//for (int j=0; j<n; ++j)
// vec[i] +=j;
/* this works */
int* val { &vec[0] };
#pragma omp parallel for reduction (+:val[i])
for (int j=0; j<n; ++j)
val[i] +=j;
/* this is allowed, but looks very wrong. Produces wrong results
* for std::vector, but on an Eigen type, it worked. */
#pragma omp parallel for reduction (+:val[i])
for (int j=0; j<n; ++j)
vec[i] +=j;
}
for (unsigned int i=0; i<vec.size(); ++i) std::cout << vec[i] << " ";
std::cout << "\n";
return 0;
}
The problem is, that if I write the reduction clause as (+:vec[i]), I get the error ‘vec’ does not have pointer or array type, which is descriptive enough to find a workaround. However, that means I have to introduce a new variable and somewhat change the code logic, and I find it less obvious to see what the code is supposed to do.
My main question is, whether there is a better/cleaner/more standard way to write a reduction for container elements.
I'd also like to know why and how the third way shown in the code above somewhat works. I'm actually working with the Eigen library, on whose containers that variant seems to work just fine (haven't extensively tested it though), but on std::vector, it produces results somewhere between zero and the actual result (8128). I thought it should work, because vec[i] and val[i] should both evaluate to dereferencing the same address. But alas, apparently not.
I'm using OpenMP 4.5 and gcc 9.3.0.
I'll answer your question in three parts:
1. What is the best way to perform to OpenMP reductions in your example above with a std::vec ?
i) Use your approach, i.e. create a pointer int* val { &vec[0] };
ii) Declare a new shared variable like #1201ProgramAlarm answered.
iii) declare a user defined reduction (which is not really applicable in your simple case, but see 3. below for a more efficient pattern).
2. Why doesn't the third loop work and why does it work with Eigen ?
Like the previous answer states you are telling OpenMP to perform a reduction sum on a memory address X, but you are performing additions on memory address Y, which means that the reduction declaration is ignored and your addition is subjected to the usual thread race conditions.
You don't really provide much detail into your Eigen venture, but here are some possible explanations:
i) You're not really using multiple threads (check n = Eigen::nbThreads( ))
ii) You didn't disable Eigen's own parallelism which can disrupt your own usage of OpenMP, e.g. EIGEN_DONT_PARALLELIZE compiler directive.
iii) The race condition is there, but you're not seeing it because Eigen operations take longer, you're using a low number of threads and only writing a low number of values => lower occurrence of threads interfering with each other to produce the wrong result.
3. How should I parallelize this scenario using OpenMP (technically not a question you asked explicitly) ?
Instead of parallelizing only the inner loop, you should parallelize both at the same time. The less serial code you have, the better. In this scenario each thread has its own private copy of the vec vector, which gets reduced after all the elements have been summed by their respective thread. This solution is optimal for your presented example, but might run into RAM problems if you're using a very large vector and very many threads (or have very limited RAM).
#pragma omp parallel for collapse(2) reduction(vsum : vec)
for (unsigned int i {0}; i<vec.size(); ++i){
for (int j = 0; j < n; ++j) {
vec[i] += j;
}
}
where vsum is a user defined reduction, i.e.
#pragma omp declare reduction(vsum : std::vector<int> : std::transform(omp_out.begin(), omp_out.end(), omp_in.begin(), omp_out.begin(), std::plus<int>())) initializer(omp_priv = decltype(omp_orig)(omp_orig.size()))
Declare the reduction before the function where you use it, and you'll be good to go
For the second example, rather than storing a pointer then always accessing the same element, just use a local variable:
int val = vec[i];
#pragma omp parallel for reduction (+:val)
for (int j=0; j<n; ++j)
val +=j;
vec[i] = val;
With the 3rd loop, I suspect that the problem is because the reduction clause names a variable, but you never update that variable by that name in the loop so there is nothing that the compiler sees to reduce. Using Eigen may make the code a bit more complicate to analyze, resulting in the loop working.
I am still confused. If i use the reduction clause in OpenMP can false sharing happen? (Both code snippets give the correct result.)
A little example, where the maximum of an array is wanted:
double max_red(double *A, int N){
double mx = std::numeric_limits<double>::min();
#pragma omp parallel for reduction(max:mx)
for(int i=0; i<N; ++i){
if(A[i]>mx) mx = A[i];
}
return mx;
}
This example can also written with extra padding
double max_padd(double *A, int N){
omp_set_num_threads(NUM_THREADS);
double local_max[NUM_THREADS][8];
double res;
#pragma omp parallel
{
int id = omp_get_thread_num();
local_max[id][0] = std::numeric_limits<double>::min();
#pragma omp for
for(int i=0; i<N; ++i){
if(A[i]>local_max[id][0])local_max[id][0]=A[i];
}
#pragma omp single
{
res = local_max[0][0];
for(int i=0; i<NUM_THREADS; ++i){
if(local_max[i][0]> res)res = local_max[i][0];
}
}
}
return res;
}
But is the extra padding necessary for totally prohibit false sharing or is the reduction clause safe enough?
Thanks
The padding is not necessary.
Technically this is not mandated by the standard. The standard doesn't say where each threads private copy is to be located in memory. Remember that false sharing is not a correctness issue, but a (very significant) practical performance issue.
However, it would be extremely surprising if any OpenMP implementation would make such a rookie mistake and place the private copies on the same cache line.
Assume that the the implementation has a better understanding of the platform and it's performance characteristics than the programmer. Only write manual "performance improvements" such as your second solution if you have proof by measurement that the idiomatic solution (e.g. your first solution) has bad performance that cannot be fixed by tuning.
Practical Note: I'm fairly sure implementations would typically place the private copy on the (private) stack of the executing thread, and afterwards each thread would update the shared variable with a critical section or atomically when supported.
In theory, a logarithmic time tree-based reduction is be possible. However implementations don't seem to do that, at least not in all cases.
I'm using OpenMP for this and I'm not confident of my answer as well. Really need your help in this. I've been wondering which method (serial or parallel) is faster in run speed in this. My #pragma commands (set into comments) are shown below.
Triangle Triangle::t_ID_lookup(Triangle a[], int ID, int n)
{
Triangle res; int i;
//#pragma omp for schedule(static) ordered
for(i=0; i<n; i++)
{
if(ID==a[i].t_ID)
{
//#pragma omp ordered
return (res=a[i]); // <-changed into "res = a[i]" instead of "return(...)"
}
}
return res;
}
It depends on n. If n is small, then the overhead required for the OMP threads makes the OMP version slower. This can be overcome by adding an if clause: #pragma omp parallel if (n > YourThreshhold)
If all a[i].t_ID are not unique, then you may receive different results from the same data when using OMP.
If you have more in your function than just a single comparison, consider adding a shared flag variable that would indicate that it was found so that a comparison if(found) continue; can be added at the beginning of the loop.
I have no experience with ordered, so if that was the crux of your question, ignore all the above and consider this answer.
Profile. In the end, there is no better answer.
If you still want a theoretical answer, then a random lookup would be O(n) with a mean of n/2 while the OMP version would be a constant n/k where k is the number of threads/cores not including overhead.
For an alternative way of writing your loop, see Z Boson's answer to a different question.
Does OpenMP natively support reduction of a variable that represents an array?
This would work something like the following...
float* a = (float*) calloc(4*sizeof(float));
omp_set_num_threads(13);
#pragma omp parallel reduction(+:a)
for(i=0;i<4;i++){
a[i] += 1; // Thread-local copy of a incremented by something interesting
}
// a now contains [13 13 13 13]
Ideally, there would be something similar for an omp parallel for, and if you have a large enough number of threads for it to make sense, the accumulation would happen via binary tree.
Array reduction is now possible with OpenMP 4.5 for C and C++. Here's an example:
#include <iostream>
int main()
{
int myArray[6] = {};
#pragma omp parallel for reduction(+:myArray[:6])
for (int i=0; i<50; ++i)
{
double a = 2.0; // Or something non-trivial justifying the parallelism...
for (int n = 0; n<6; ++n)
{
myArray[n] += a;
}
}
// Print the array elements to see them summed
for (int n = 0; n<6; ++n)
{
std::cout << myArray[n] << " " << std::endl;
}
}
Outputs:
100
100
100
100
100
100
I compiled this with GCC 6.2. You can see which common compiler versions support the OpenMP 4.5 features here: https://www.openmp.org/resources/openmp-compilers-tools/
Note from the comments above that while this is convenient syntax, it may invoke a lot of overheads from creating copies of each array section for each thread.
Only in Fortran in OpenMP 3.0, and probably only with certain compilers.
See the last example (Example 3) on:
http://wikis.sun.com/display/openmp/Fortran+Allocatable+Arrays
Now the latest openMP 4.5 spec has supports of reduction of C/C++ arrays.
http://openmp.org/wp/2015/11/openmp-45-specs-released/
And latest GCC 6.1 also has supported this feature.
http://openmp.org/wp/2016/05/gcc-61-released-supports-openmp-45/
But I didn't give it a try yet. Wish others can test this feature.
OpenMP cannot perform reductions on array or structure type variables (see restrictions).
You also might want to read up on private and shared clauses. private declares a variable to be private to each thread, where as shared declares a variable to be shared among all threads. I also found the answer to this question very useful with regards to OpenMP and arrays.
OpenMP can perform this operation as of OpenMP 4.5 and GCC 6.3 (and possibly lower) supports it. An example program looks as follows:
#include <vector>
#include <iostream>
int main(){
std::vector<int> vec;
#pragma omp declare reduction (merge : std::vector<int> : omp_out.insert(omp_out.end(), omp_in.begin(), omp_in.end()))
#pragma omp parallel for default(none) schedule(static) reduction(merge: vec)
for(int i=0;i<100;i++)
vec.push_back(i);
for(const auto x: vec)
std::cout<<x<<"\n";
return 0;
}
Note that omp_out and omp_in are special variables and that the type of the declare reduction must match the vector you are planning to reduce on.