为什么OpenMP比顺序程序慢一些以简化操作?

时间:2018-10-16 17:12:28

标签: c++ openmp

我正尝试在数组中查找元素的总和,如下所示。但是,令人惊讶的是,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)。我正在使用g++ -std=c++11 main.cpp -fopenmp -O3 && ./a.out

在Ubuntu 16.04上进行编译

3 个答案:

答案 0 :(得分:4)

顺序程序可以优化为无所事事。这是因为唯一的副作用是sum的值,而sum的值在您的程序中是不可观察的。

使用OpenMP时,复杂的线程处理使编译器无法意识到您没有做任何事情。

可以避免这种情况的简单方法是添加return sum;,现在它显示为退出代码,可以观察到,因此无法优化计算。

现在,编译器仍然可以自由地从不分配ary,因为它可以证明所有ary[i]==ii,而只用{{1 }},然后在编译时计算从ary[i]i的{​​{1}}的总和为i,消除整个循环并将其设为1,然后仍然需要零时间。

答案 1 :(得分:2)

事先备注:
我相信处理“手动”划分循环的方式会适得其反(除非您想了解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();
}

具有复杂的CPU密集型操作符号的测试函数(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

测量
时间测量是通过chronohigh_precision_clock完成的,并且我的机器上的极限精度为微秒,因此使用std::chrono::microseconds(毫无意义地寻找更高的精度)

简单操作图(轴为对数刻度!)
CPU simple

复杂操作图(轴为对数刻度!)
CPU intensive

得出的结论

  • 因为必须将池线程设置在适当的位置,所以第一次使用OpenMP会产生偏移(第一个#pragma omp越过)。
    如果我们第一次进入“ test_”函数(i = 128)时仔细研究“密集型案例”,则在OpenMP案例中的时间成本要比在No OpenMP案例中的时间成本高得多。在第二次调用中(i = 256),我们没有看到使用OpenMP的好处,但是时间安排是一致的。 CPU intensive close up
  • 我们可以看到,在少量样本中我们没有观察到可伸缩性。在简单的测试案例中,这一点更加清楚。换句话说,并行部分中的操作量必须足够高,以使线程池管理所需的时间可以忽略不计。否则,将操作分为线程是没有意义的。

  • 在这种情况下(使用我使用的处理器),最小样本数约为100000。但是,如果我使用256个线程,则肯定约为600万。

  • 但是对于使用OpenMP进行更多CPU密集型操作,即使使用1000个样本(使用我使用的处理器)也可以加快速度

摘要

  • 如果您安装OpenMP 代码,请尝试通过 #pragma omp parallel 的简单操作预先设置池线程。在您的测试案例中,设置花费了大部分时间。
  • 仅当您并行化足够占用CPU的功能时才使用OpenMP(这并不是简单的数组求和的情况……)。例如,这就是嵌套循环的原因#pragma omp for应该始终位于最外面的“可能”循环中。

答案 2 :(得分:1)

如Max Langhof和user463035818所建议,该程序受内存限制。我更改了程序,以完成除累加之外的其他操作。也就是说,我将sum += ary[i]更改为sum += (pow(ary[i], 1.1) + pow(ary[i], 1.2)) / 100000000.0,并在并行程序中执行了相同的更改并测量了时间。并行程序的速度提高了2倍。如果该程序受IO限制,我想我不能做很多事情来使它与OpenMP一起更快。否则,请告诉我。