Почему OpenMP медленнее, чем последовательная программа для простого сокращения? - PullRequest
0 голосов
/ 16 октября 2018

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

#include <iostream>
#include <omp.h>
int main() {
  int N = 10000;
  int * ary = new int[N];
  for (int i = 0; i < N; i++) { input_file >> ary[i]; }
  int sum = 0;
  clock_t begin = clock();
  for (int i = 0; i < N; i++) { sum += ary[i]; }
  clock_t end = clock();
  cout << sum;
  double elapsed_time = double(end - begin) / CLOCKS_PER_SEC;
  sum = 0;
  begin = clock();
  #pragma omp parallel
  {
    int thread_id = omp_get_thread_num();
    int total_threads = omp_get_num_threads();
    int elem_per_thread = N / total_threads;
    int base = thread_id * elem_per_thread;
    int internal_sum = 0;
    for (int i = base; i < (base + elem_per_thread); i++) {
      internal_sum += ary[i];
    }
    #pragma omp critical
    {
      sum += internal_sum;
    }
  }
  end = clock();
  cout << sum;
  elapsed_time = double(end - begin) / CLOCKS_PER_SEC;    
}

Для последовательной программы требуется 5e-06 (s), а для параллельной программы - 0.001733 (s).Я собираю на Ubuntu 16.04, используя g++ -std=c++11 main.cpp -fopenmp -O3 && ./a.out

Ответы [ 3 ]

0 голосов
/ 16 октября 2018

Замечание заранее:
Обработка способа разделения цикла «вручную», я считаю, контрпродуктивна (если вы не хотите понять, как работает OpenMP).Вот почему я сначала предлагаю вам использовать более стандартный подход с операцией reduction.Вы всегда можете убедиться, что он дает одинаковый результат с точки зрения производительности.
Еще одно замечание: использование всего кода omp_ функций не позволит вам скомпилировать его без опции -openmp.

Скамья

Итак, я набрал следующий код:

Жатки

#include <iostream>
#include <fstream>
#include <omp.h>
#include <cmath>
#include <chrono>
#include <iomanip>

. тестовая функция с очень простой операцией добавления

void test_simple(long long int N, int * ary, double & sum, long long int & elapsed_milli)
{
  std::chrono::time_point<std::chrono::high_resolution_clock> start, end;
  start = std::chrono::system_clock::now();
  double local_sum = 0.0;
  #pragma omp parallel
  {
    #pragma omp for reduction(+:local_sum)
    for (long long int i = 0; i < N; i++) {
      local_sum += ary[i];
    }
  }
  sum = local_sum;
  end = std::chrono::system_clock::now();
  elapsed_milli = std::chrono::duration_cast<std::chrono::microseconds>
                             (end-start).count();
}

. тестовая функция со сложным, интенсивно использующим процессор знаком (x) atan (sqrt (cos (x) ^ 2 + sin (0.5x) ^ 2)

void test_intensive(long long int N, int * ary, double & sum, long long int & elapsed_milli)
{
  std::chrono::time_point<std::chrono::high_resolution_clock> start, end;
  start = std::chrono::system_clock::now();
  double local_sum = 0.0;
  #pragma omp parallel
  {
    double c, s;
    #pragma omp for reduction(+:local_sum)
    for (long long int i = 0; i < N; i++) {
      c = cos(double(ary[i]));
      s = sin(double(ary[i])*0.5);
      local_sum += atan(sqrt(c*c+s*s));
    }
  }
  sum = local_sum;
  end = std::chrono::system_clock::now();
  elapsed_milli = std::chrono::duration_cast<std::chrono::microseconds>
                             (end-start).count();  
}

. Основная функция

using namespace std;
int main() {
  long long int N = 1073741825,i;
  int * ary = new int[N];
  srand (0);
  for (i = 0; i < N; i++) { ary[i] = rand()-RAND_MAX/2; }
  double sum = 0.0;
  sum = 0.0;
  long long int  elapsed_milli;
  cout <<"#"<<setw(19)<<"N"<<setw(20)<<"µs"<< endl;
  for(i=128; i<N; i=i*2)
  {
      test_intensive(i, ary, sum, elapsed_milli);
      //test_simple(i, ary, sum, elapsed_milli);
      cout << setw(20)<<i<<setw(20)<<elapsed_milli << setw(20)<<sum<<endl;
  }
}

Скомпилировать (используя icpc)
Последовательная (без OpenMP) версия скомпилирована с:

icpc test_omp.cpp -O3 --std=c++0x  

OpenMP (OpenMP) версия скомпилирована с:

icpc test_omp.cpp -O3 --std=c++0x -openmp

Измерение
Измерения времени выполняются с chrono с использованием high_precision_clock, а точность предела на моей машине составляет микросекунды, следовательно, используется std::chrono::microseconds (нетточка ищет более высокую точность)

График для простой операции (ось в логарифмическом масштабе!)
CPU simple

График для сложной операции (оси в логарифмическом масштабе!)
CPU intensive

Сделанные выводы

  • При первом использовании OpenMP существует смещение (первое #pragma omp пересечено), поскольку поток пула должен быть установлен на месте.
    Если мы более внимательно посмотрим на «интенсивный случай»'При первом входе в функцию test_ (с i = 128) затраты времени в случае OpenMP намного выше, чем в случае отсутствия OpenMP.Во втором вызове (с i = 256) мы не видим преимущества использования OpenMP, но время согласовано.CPU intensive close up
  • Мы видим, что мы не наблюдаем масштабируемость с небольшим количеством выборок.Это проще в простом тестовом примере.Другими словами количество операций внутри параллельной секции должно быть достаточно большим, чтобы время, необходимое для управления пулом потоков, было пренебрежимо малым .В противном случае нет смысла делить операцию на потоки.

  • В этом случае (с процессором, который я использовал) минимальное количество выборок составляет около 100000. Но если бы я использовал 256 потоковоно наверняка будет около 6000000.

  • Однако для более ресурсоемких операций с использованием OpenMP можно ускорить работу даже с 1000 семплами (с процессором, который я использовал)

Сводка

  • Если вы работаете с кодом OpenMP , попробуйте предварительно настроить поток пула с помощью простой операции с # pragmaпомпа параллельная .В вашем тестовом примере настройка занимает большую часть времени.
  • Использование OpenMP является уловкой, только если вы распараллеливаете достаточно интенсивно загружающие ЦП функции (что на самом деле не является простой суммой массива ...). Например, именно по этой причине во вложенных циклах #pragma omp for всегда должен находиться во внешнем «возможном» цикле.
0 голосов
/ 16 октября 2018

Как предположили Макс Лангхоф и пользователь 463035818, программа ограничена в памяти.Я изменил программу, чтобы сделать что-то большее, чем накопление.То есть я изменил sum += ary[i] на sum += (pow(ary[i], 1.1) + pow(ary[i], 1.2)) / 100000000.0, выполнил то же изменение в параллельной программе и измерил время.Параллельная программа стала в 2 раза быстрее.Если программа ограничена вводом-выводом, я думаю, что я ничего не могу с этим поделать, чтобы сделать ее быстрее с OpenMP.Пожалуйста, дайте мне знать, если вы думаете иначе.

0 голосов
/ 16 октября 2018

Последовательная программа оптимизирует работу до бездействия.Это связано с тем, что единственным побочным эффектом является значение sum, а значение sum не наблюдается в вашей программе.

При использовании OpenMP сложность многопоточности не позволяет компилятору реализоватьВы ничего не делаете.

Простой способ избежать этого - добавить return sum;, теперь он отображается как код выхода, который можно наблюдать, и, следовательно, расчет не может быть оптимизирован.

Теперь компилятор по-прежнему свободен никогда не выделять ary, потому что он может доказать, что ary[i]==i для всех i, и заменить чтение ary[i] просто i, а затем вычислить во время компиляции, чтосумма i от 1 до 10000 равна 50005000, исключите весь ваш цикл и сделайте его sum=50005000 и все равно займет нулевое время.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...