Проблема воспроизводимости вывода 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("\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)

Я также пытался добавить барьер, но все равно получаю разные ответы, хотя и ближе к серийной версии, все еще с разбросом значений и не идентичными...

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
165
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я считаю, что проблема заключается в объявлении 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++)
    {...}

Результат должен быть идентичным, но теперь вы используете столько потоков, сколько доступно на реальном оборудовании. Это уменьшает переключение контекста между потоками и должно повысить производительность кода.

Привет, Лейтон, большое спасибо, ты был прав, твое предложение решило проблему. Спасибо еще раз !

brasamical 09.04.2019 19:53

Привет @leighton-ritchie У меня есть дополнительный вопрос относительно приведенного выше кода. Прежде всего, я подтверждаю, что везде, где я выполняю ./a.out (то есть просто интерактивный запуск), исправление, которое вы предоставили, действительно дает одно и то же число. Но я провел эксперимент и вместо того, чтобы просто выполнить ./a.out, я сделал ./a.out > output.txt, затем tail -10 output.txt. Затем я замечаю, что напечатанное окончательное значение («общая сумма после цикла») может время от времени отличаться. Знаете ли вы, может ли каким-либо образом перенаправление на файл ухудшить процесс? Должен ли я затем писать в файл, находясь внутри кода?

brasamical 10.04.2019 17:04

Нет никаких причин для того, чтобы конвейерный вывод отличался. На сколько знаков после запятой оно «время от времени отличается»? Я предполагаю, что это за 6-м десятичным знаком. Я также предполагаю, что использование «общей суммы после цикла» означает, что вы ее не сортировали. Если это так, это подвергает вашу программу (возможным) числовым ошибкам, как упоминалось после первого блока кода в моем ответе. Вероятно, если запустить интерактивную программу еще раз, то окажется, что это не совсем тот же самый float. Вы можете увидеть больше здесь

Leighton Ritchie 11.04.2019 04:38

Проблема связана с переменными 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» перед токеном «=»

brasamical 10.04.2019 16:44

Я думаю, что проблема исходит из пространства до #pragma . У меня нет проблем с компиляцией с помощью gcc, и я предпочитаю делать отступы, чтобы показать логику кода, но стандарт C говорит, что эти директивы должны начинаться с нулевого столбца строки, а другие компиляторы могут быть более строгими. Попробуйте удалить все пробелы, чтобы начать строку с '#', и это должно сработать.

Alain Merigot 10.04.2019 18:32

Итак, просто чтобы поделиться решением, которое я нашел для обхода проблемы, я заменил summ=summ+argg на summ+=argg, и он перестал выдавать ошибку, а код с модификацией, которой вы поделились, работает правильно. Во время просмотра я обнаружил, что некоторые версии (чего? gcc? ОС? openMP? не объяснено) требуют специально сжатого синтаксиса для приращения, следующего за #pragma omp Critical. (PS: пожалуйста, проголосуйте за вопрос, если вы считаете его полезным для сообщества)

brasamical 10.04.2019 22:07

Верно. Хотя обе формы эквивалентны, некоторые компиляторы требуют, чтобы операция чтения-изменения-записи отображалась явно.

Alain Merigot 10.04.2019 22:25

Другие вопросы по теме