#c #unix #openmp
#c #unix #openmp
Вопрос:
Я просматриваю учебные пособия по 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("tt and summ is %f n",summ);
}
printf("ntotal summ after loop: %fn",summ);
}
Я компилирую этот код на RedHat, используя gcc -f mycode.c -fopenmp, и когда я запускаю его, скажем, 3 раза, я получаю:
3.117
3.113
3.051
Кто-нибудь может помочь понять, почему я получаю разные результаты? Я делаю что-то не так? Параллелизм просто увеличивает интервал интегрирования, но поскольку прямоугольники вычисляются, он должен быть одинаковым, когда они суммируются в конце, нет?
последовательная версия дает мне 3,13
(тот факт, что я не получаю 3.14, является нормальным, потому что я использовал очень грубую выборку интеграла всего с 200 делениями между 0 и 1)
Я также пытался добавить барьер, но я все еще получаю разные ответы, хотя и ближе к серийной версии, все еще с разбросом значений и не идентичные…
Ответ №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("tt and summ is %f n", summ);
}
printf("ntotal summ after loop: %fn", 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: %.9fn", testSum);
std::sort(arr, arr nb);
testSum = 0;
for (int i = 0; i < nb; i )
testSum = arr[i];
printf("sorted sum: %.9fn", testSum);
testSum = 0;
for (int i = nb-1; i >= 0; i--)
testSum = arr[i];
printf("reversed sum: %.9fn", 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 )
{...}
Результат должен быть идентичным, но теперь вы используете только столько потоков, сколько доступно на реальном оборудовании. Это уменьшает переключение контекста между потоками и должно повысить производительность вашего кода по времени.
Комментарии:
1. Привет, Лейтон, большое спасибо, вы были правы, ваше предложение решило проблему. Еще раз спасибо !
2. Привет @leighton-ritchie, у меня есть дополнительный вопрос относительно приведенного выше кода. Прежде всего, я подтверждаю, что везде, где я делаю ./a.out (то есть, просто простой интерактивный запуск), предоставленное вами исправление действительно дает то же самое число. Но я провел эксперимент и вместо того, чтобы просто выполнить./a.out, я сделал ./a.out > output.txt, затем хвост -10 output.txt . Затем я замечаю, что напечатанное конечное значение («общая сумма после цикла») может время от времени действительно отличаться. Вы знаете, может ли каким-либо образом перенаправление на файл ухудшить процесс? Должен ли я затем записывать в файл, находясь внутри кода?
3. Нет никаких причин для того, чтобы вывод по каналу отличался. На сколько знаков после запятой это «отличается время от времени»? Я предполагаю, что это за 6-м знаком после запятой. Я также предполагаю, что использование «total summ after loop» означает, что вы его не отсортировали. Если это так, это подвергает вашу программу (возможным) числовым ошибкам, как упоминалось после первого блока кода в моем ответе. Вероятно, при повторном запуске интерактивной программы выяснилось бы, что на самом деле это был не совсем тот же float. Вы можете увидеть больше здесь
Ответ №2:
Проблема возникает из-за переменных summ
, argg
и i
. Они принадлежат глобальной последовательной области видимости и не могут быть изменены без мер предосторожности. У вас будут гонки между потоками, и это может привести к неожиданным значениям в этих переменных. Гонки полностью неопределенны, и это объясняет разные результаты, которые вы получаете. Вы также можете получить правильный результат или любой неправильный результат в зависимости от временных вхождений операций чтения и записи в эти переменные.
Правильный способ решения этой проблемы :
-
для переменных
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("tt and summ is %f n",summ);
}
printf("ntotal summ after loop: %fn",summ);
}
Как уже отмечалось, это не лучший способ использования потоков. Создание и синхронизация потоков обходятся дорого, и редко требуется иметь больше потоков, чем количество ядер.
Комментарии:
1. Здравствуйте @alain-merigot, большое спасибо за то, что нашли время для всестороннего объяснения проблемы. У меня действительно есть вопрос относительно написанного вами кода: когда я его запускаю, pragma atomic, похоже, выдает ошибку. Я получаю: ошибка: недопустимый оператор для ‘#pragma omp atomic’ перед ‘=’ token’
2. Я думаю, что проблема возникает из предыдущего пробела
#pragma
. У меня нет проблем с компиляцией с помощью gcc, и я предпочитаю делать отступы, чтобы показать логику кода, но стандарт C гласит, что эти директивы должны начинаться со столбца 0 строки, а другие компиляторы могут быть более строгими. Попробуйте удалить все пробелы, чтобы начать строку с'#'
, и это должно сработать.3. Хорошо, просто чтобы поделиться решением, которое я нашел для обхода проблемы, я заменил summ = summ argg на summ = argg, и он перестал выдавать ошибку, и код с изменениями, которыми вы поделились, после этого работает должным образом. Во время просмотра я обнаружил, что некоторые версии (чего? gcc? ОПЕРАЦИОННАЯ система? OpenMP? не объяснено) требует, в частности, сжатого синтаксиса для увеличения, следующего за критическим значением #pragma omp. (PS: пожалуйста, поддержите вопрос, если вы считаете его полезным для сообщества)
4. Верно. Хотя обе формы эквивалентны, некоторые компиляторы требуют, чтобы операция чтения-изменения-записи отображалась явно.