Это классический пример условия гонки . Каждый из ваших потоков openmp одновременно обращается к общему значению и обновляет его, и нет никакой гарантии, что некоторые обновления не будут потеряны (в лучшем случае) или полученный ответ не будет напрасным (в худшем случае).
С условиями гонки дело в том, что они чувствительно зависят от времени; в меньшем случае (например, с меньшими NREC и NLIG) вы можете иногда пропустить это, но в большем случае это в конечном итоге всегда будет появляться.
Причина, по которой вы получаете неправильные ответы без #pragma omp for
, заключается в том, что как только вы входите в параллельную область, все ваши потоки openmp запускаются; и если вы не используете что-то вроде omp for
(так называемая конструкция разделения рабочих мест), чтобы разделить работу, каждый поток будет делать все в параллельном разделе - так что все потоки будут делать все то же самое сумма, все обновления S2
одновременно.
Вы должны быть осторожны с потоками OpenMP, обновляющими общие переменные. OpenMP имеет atomic
операций, позволяющих безопасно изменять общую переменную. Ниже приведен пример (к сожалению, ваш пример настолько чувствителен к порядку суммирования, что трудно понять, что происходит, поэтому я немного изменил вашу сумму :). В mysumallatomic
каждый поток обновляет S2
, как и раньше, но на этот раз все безопасно:
#include <omp.h>
#include <math.h>
#include <stdio.h>
double mysumorig() {
double S2 = 0;
int a, b;
for(a=0;a<128;a++){
for(b=0;b<128;b++){
S2=S2+a*b;
}
}
return S2;
}
double mysumallatomic() {
double S2 = 0.;
#pragma omp parallel for shared(S2)
for(int a=0; a<128; a++){
for(int b=0; b<128;b++){
double myterm = (double)a*b;
#pragma omp atomic
S2 += myterm;
}
}
return S2;
}
double mysumonceatomic() {
double S2 = 0.;
#pragma omp parallel shared(S2)
{
double mysum = 0.;
#pragma omp for
for(int a=0; a<128; a++){
for(int b=0; b<128;b++){
mysum += (double)a*b;
}
}
#pragma omp atomic
S2 += mysum;
}
return S2;
}
int main() {
printf("(Serial) S2 = %f\n", mysumorig());
printf("(All Atomic) S2 = %f\n", mysumallatomic());
printf("(Atomic Once) S2 = %f\n", mysumonceatomic());
return 0;
}
Тем не менее, эта атомарная операция действительно вредит параллельной производительности (в конце концов, все дело в предотвращении параллельной операции вокруг переменной S2
!), Поэтому лучшим подходом является суммирование и выполнение только атомарная операция после обоих суммирований вместо 128 * 128 раз; это подпрограмма mysumonceatomic()
, которая вызывает накладные расходы на синхронизацию только один раз на поток, а не 16 000 раз на поток.
Но это настолько распространенная операция, что нет необходимости самому ее выполнять. Можно использовать встроенную функциональность OpenMP для операций редукции (редукция - это операция, такая как вычисление суммы списка, нахождение минимума или максимума списка и т. Д., Которая может выполняться только по одному элементу за раз, только глядя на результат пока и следующий элемент), как предложено @ejd. OpenMP будет работать и работать быстрее (его оптимизированная реализация намного быстрее, чем вы можете делать самостоятельно с другими операциями OpenMP).
Как видите, любой подход работает:
$ ./foo
(Serial) S2 = 66064384.000000
(All Atomic) S2 = 66064384.000000
(Atomic Once) S2 = 66064384.00000