Я просматриваю учебные пособия по openMP, и по мере моего продвижения я написал версию кода для openMP, которая вычисляет PI с использованием интеграла.
Я написал серийную версию, поэтому я знаю, что серийный аналог в порядке. После того, как версия openMP была завершена, я заметил, что каждый раз, когда я запускаю ее, она дает мне другой ответ. Если я сделаю несколько запусков, я увижу, что выходные данные в целом соответствуют правильному числу, но все же я не ожидал, что несколько запусков openMP дадут разные ответы.
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>
void main()
{ int nb=200,i,blob;
float summ=0,dx,argg;
dx=1./nb;
printf("\n dx------------: %f \n",dx);
omp_set_num_threads(nb);
#pragma omp parallel
{
blob=omp_get_num_threads();
printf("\n we have now %d number of threads...\n",blob);
int ID=omp_get_thread_num();
i=ID;
printf("\n i is now: %d \n",i);
argg=(4./(1.+i*dx*i*dx))*dx;
summ=summ+argg;
printf("\t\t and summ is %f \n",summ);
}
printf("\ntotal summ after loop: %f\n",summ);
}
Я компилирую этот код на RedHat, используя gcc -f mycode.c -fopenmp, и когда я запускаю его, скажем, 3 раза, я получаю:
3.117
3.113
3.051
Может ли кто-нибудь помочь понять, почему я получаю разные результаты? Я делаю что-то неправильно? Параллелизм просто соединяет интервал интегрирования, но поскольку прямоугольники вычисляются, они должны быть одинаковыми, когда они суммируются в конце, не так ли?
серийная версия дает мне 3.13
(то, что я не получаю 3,14, нормально, потому что я использовал очень грубую выборку интеграла всего с 200 делениями от 0 до 1)
Я также пытался добавить барьер, но все равно получаю разные ответы, хотя и ближе к серийной версии, все еще с разбросом значений и не идентичными...
Я считаю, что проблема заключается в объявлении int i
и float argg
вне параллельного цикла.
Происходит то, что все ваши 200 потоков перезаписывают i
и argg
, поэтому иногда argg
потока перезаписывается argg
из другого потока, что приводит к непредсказуемой ошибке, которую вы наблюдаете.
Вот рабочий код, который всегда печатает одно и то же значение (до 6 знаков после запятой или около того):
void main()
{
int nb = 200, blob;
float summ = 0, dx;// , argg;
dx = 1. / nb;
printf("\n dx------------: %f \n", dx);
omp_set_num_threads(nb);
#pragma omp parallel
{
blob = omp_get_num_threads();
printf("\n we have now %d number of threads...\n", blob);
int i = omp_get_thread_num();
printf("\n i is now: %d \n", i);
float argg = (4. / (1. + i * dx*i*dx))*dx;
summ = summ + argg;
printf("\t\t and summ is %f \n", summ);
}
printf("\ntotal summ after loop: %f\n", summ);
}
Однако изменение последней строки на %.9f показывает, что на самом деле это не то же самое число с плавающей запятой. Это связано с числовыми ошибками в сложениях с плавающей запятой. a+b+c не гарантирует того же результата, что и a+c+b. Вы можете попробовать это в следующем примере:
Сначала добавьте float* arr = new float[nb];
до параллельный цикл И arr[i] = argg;
в пределах параллельный цикл, после того, как argg
определено, конечно. Затем добавьте следующий после параллельный цикл:
float testSum = 0;
for (int i = 0; i < nb; i++)
testSum += arr[i];
printf("random sum: %.9f\n", testSum);
std::sort(arr, arr + nb);
testSum = 0;
for (int i = 0; i < nb; i++)
testSum += arr[i];
printf("sorted sum: %.9f\n", testSum);
testSum = 0;
for (int i = nb-1; i >= 0; i--)
testSum += arr[i];
printf("reversed sum: %.9f\n", testSum);
Скорее всего, отсортированная сумма и обратная сумма немного отличаются, хотя они составлены путем сложения одних и тех же 200 чисел.
Еще одна вещь, которую вы, возможно, захотите отметить, это то, что вы вряд ли найдете процессор, который может фактически выполнять 200 потоков параллельно. Наиболее распространенные процессоры могут обрабатывать от 4 до 32 потоков, в то время как специализированные серверные процессоры могут обрабатывать до 112 потоков с Xeon Platinum 9282 за 15 тысяч долларов.
Поэтому мы обычно делаем следующее:
Убираем omp_set_num_threads(nb);
, чтобы использовать рекомендуемое количество потоков
Мы удаляем int i = omp_get_thread_num();
, чтобы использовать int i
из цикла for
Перепишем цикл как цикл for:
#pragma omp parallel for
for (int i = 0; i < nb; i++)
{...}
Результат должен быть идентичным, но теперь вы используете столько потоков, сколько доступно на реальном оборудовании. Это уменьшает переключение контекста между потоками и должно повысить производительность кода.
Привет @leighton-ritchie У меня есть дополнительный вопрос относительно приведенного выше кода. Прежде всего, я подтверждаю, что везде, где я выполняю ./a.out (то есть просто интерактивный запуск), исправление, которое вы предоставили, действительно дает одно и то же число. Но я провел эксперимент и вместо того, чтобы просто выполнить ./a.out, я сделал ./a.out > output.txt, затем tail -10 output.txt. Затем я замечаю, что напечатанное окончательное значение («общая сумма после цикла») может время от времени отличаться. Знаете ли вы, может ли каким-либо образом перенаправление на файл ухудшить процесс? Должен ли я затем писать в файл, находясь внутри кода?
Нет никаких причин для того, чтобы конвейерный вывод отличался. На сколько знаков после запятой оно «время от времени отличается»? Я предполагаю, что это за 6-м десятичным знаком. Я также предполагаю, что использование «общей суммы после цикла» означает, что вы ее не сортировали. Если это так, это подвергает вашу программу (возможным) числовым ошибкам, как упоминалось после первого блока кода в моем ответе. Вероятно, если запустить интерактивную программу еще раз, то окажется, что это не совсем тот же самый float. Вы можете увидеть больше здесь
Проблема связана с переменными summ
, argg
и i
. Они относятся к глобальной последовательной области видимости и не могут быть изменены без соблюдения мер предосторожности. У вас будут гонки между потоками, и это может привести к неожиданным значениям в этих var. Гонки совершенно недетерминированы, и это объясняет разные результаты, которые вы получаете. Вы также можете получить правильный результат или любой неправильный результат в зависимости от временных вхождений чтения и записи в эти переменные.
Правильный способ решения этой проблемы:
для переменных argg
и i
: они объявлены в глобальной области видимости, но используются для выполнения временных вычислений в потоках. Вы должны: либо объявить их в параллельном домене, чтобы сделать их частными потоками, либо добавить private(argg,i)
в директиву omp. Обратите внимание, что существует также потенциальная проблема для blob
, но его значение одинаково во всех потоках, и это не должно влиять на поведение программы.
для переменной summ
ситуация иная. Это В самом деле — глобальная переменная, которая накапливает некоторые значения из потоков. Он должен оставаться глобальным, но вы должны добавить директиву atomic
openmp при его изменении. Полная операция чтения-модификации-записи для переменной станет нерушимой, и это обеспечит модификацию без гонок.
Вот модифицированная версия вашего кода, которая дает согласованный результат (но поплавки не являются ассоциативными, и последнее десятичное число может измениться).
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>
void main()
{
int nb=200,i,blob;
float summ=0,dx,argg;
dx=1./nb;
printf("\n dx------------: %f \n",dx);
omp_set_num_threads(nb);
# pragma omp parallel private(argg,i)
{
blob=omp_get_num_threads();
printf("\n we have now %d number of threads...\n",blob);
int ID=omp_get_thread_num();
i=ID;
printf("\n i is now: %d \n",i);
argg=(4./(1.+i*dx*i*dx))*dx;
#pragma omp atomic
summ=summ+argg;
printf("\t\t and summ is %f \n",summ);
}
printf("\ntotal summ after loop: %f\n",summ);
}
Как уже отмечалось, это не лучший способ использования потоков. Создание и синхронизация потоков требует больших затрат, и редко требуется иметь больше потоков, чем количество ядер.
Здравствуйте @alain-merigot, большое спасибо, что нашли время, чтобы всесторонне объяснить проблему. У меня есть вопрос относительно кода, который вы написали: когда я запускаю его, прагма atomic выдает ошибку. Я получаю: ошибка: недопустимый оператор для «#pragma omp atomic» перед токеном «=»
Я думаю, что проблема исходит из пространства до #pragma
. У меня нет проблем с компиляцией с помощью gcc, и я предпочитаю делать отступы, чтобы показать логику кода, но стандарт C говорит, что эти директивы должны начинаться с нулевого столбца строки, а другие компиляторы могут быть более строгими. Попробуйте удалить все пробелы, чтобы начать строку с '#'
, и это должно сработать.
Итак, просто чтобы поделиться решением, которое я нашел для обхода проблемы, я заменил summ=summ+argg на summ+=argg, и он перестал выдавать ошибку, а код с модификацией, которой вы поделились, работает правильно. Во время просмотра я обнаружил, что некоторые версии (чего? gcc? ОС? openMP? не объяснено) требуют специально сжатого синтаксиса для приращения, следующего за #pragma omp Critical. (PS: пожалуйста, проголосуйте за вопрос, если вы считаете его полезным для сообщества)
Верно. Хотя обе формы эквивалентны, некоторые компиляторы требуют, чтобы операция чтения-изменения-записи отображалась явно.
Привет, Лейтон, большое спасибо, ты был прав, твое предложение решило проблему. Спасибо еще раз !