使用openmp

时间:2015-08-01 04:58:12

标签: c++ multithreading openmp

我需要计算对象向量中所有元素i,j之间的交互。在大小为N的向量中,这相当于(N*(N-1))/2次计算,并且可以在嵌套的for循环中天真地求解,如下所示:

for( unsigned int i = 0; i < vector.size()-1; i++ ) {
  for ( unsigned int j = i+1; j < vector.size(); j++ ) {
    //compute interaction between vector[i] and vector[j]
  }
}

使用OpenMP并行化尝试加快进程的难度。随着i的增加,内环中的计算次数呈线性减少。据我了解,#pragma omp parallel for将循环除以所使用的线程数。虽然外环将被均分,但实际计算不会。例如,长度为257的向量将具有(257 * 256)/ 2 = 32896个计算。如果OpenMP均匀地分割外部循环(线程1具有i = 0 ... 127,线程2具有i = 128 ... 255),则线程1将必须计算24640个交互,而线程2将必须计算8256个交互,长约75%,总效率为62%。将外环分成4个螺纹需要约44%的效率,效率约为57%。我可以验证这是MCVE的一个问题

#include <iostream>
#include <unistd.h>
#include <omp.h>
#include <vector>
#include <ctime>

int main()
{
  timespec sleepTime;
  sleepTime.tv_sec = 0;
  sleepTime.tv_nsec = 1e6; // 1 ms                             
  std::vector< int > dummyVector(257,0);
  #pragma omp parallel for
  for(unsigned int i = 0; i < dummyVector.size()-1; i++ ) {
    for(unsigned int j = i+1; j < dummyVector.size(); j++ ) {
      // calculate( dummyVector[i], dummyVector[j] );
      nanosleep(&sleepTime,NULL);
    }
  }
  return 0;
}

使用nanosleep模拟我的交互,2线程和4线程版本分别占75%和44%

[me@localhost build]$ export OMP_NUM_THREADS=1
[me@localhost build]$ time ./Temp

real    0m38.242s ...
[me@localhost build]$ export OMP_NUM_THREADS=2
[me@localhost build]$ time ./Temp

real    0m28.576s ...
[me@localhost build]$ export OMP_NUM_THREADS=4
[me@localhost build]$ time ./Temp

real    0m16.715s ... 

如何更好地平衡线程间的计算?有没有办法告诉OpenMP不连续地拆分外部循环?

为了将嵌套的for循环移出omp并行块,我尝试预先计算所有可能的索引对,然后循环遍历这些对

  std::vector< std::pair < int, int > > allPairs;
  allPairs.reserve((dummyVector.size()*(dummyVector.size()-1))/2);
  for(unsigned int i = 0; i < dummyVector.size()-1; i++ ) {
    for(unsigned int j = i+1; j < dummyVector.size(); j++ ) {
      allPairs.push_back(std::make_pair<int,int>(i,j));
    }
  }

  #pragma omp parallel for
  for( unsigned int i = 0; i < allPairs.size(); i++ ) {
    // calculate( dummyVector[allPairs[i].first], 
    //   dummyVector[allPairs[i].second] ); 
    nanosleep(&sleepTime,NULL);
  }

这确实有效地平衡了线程之间的计算,但它引入了索引对的不可避免的串行构造,这会在N增长时损害我的运行时。我能做得比这更好吗?

2 个答案:

答案 0 :(得分:3)

正如@HighPerformanceMark所建议的那样,解决方案在于调度OpenMP并行for循环。 Lawrence Livermore OpenMP tutorial对不同选项有很好的描述,但一般语法是#pragma parallel for schedule(type[,chunk]),其中chunk参数是可选的。如果未指定计划,则默认值是特定于实现的。对于libgomp,默认值为STATIC,它将循环迭代均匀且连续地分开,导致此问题的负载平衡不佳。

另外两个调度选项以稍高的开销为代价来修复负载均衡问题。第一个是DYNAMIC,它在线程完成其先前的工作时动态地为每个线程分配一个块(默认块大小为1循环迭代)。因此代码看起来像这样

#pragma omp parallel for schedule( dynamic )
  for(unsigned int i = 0; i < dummyVector.size()-1; i++ ) {
    for(unsigned int j = i+1; j < dummyVector.size(); j++ ) {
      // calculate( dummyVector[i], dummyVector[j]);
    }
  }

因为内循环的计算成本是结构化的(随着i的增加呈线性减少),所以GUIDED计划也很有效。它还为每个线程动态分配工作块,但它从较大的块开始,随着计算的继续减少块大小。分配给线程的第一个迭代块大小为number_iterations/number_threads,每个后续块的大小为remaining_iterations/number_threads。这确实需要反转外循环的顺序,因此初始迭代包含的工作量最少。

#pragma omp parallel for schedule( guided )
  for(unsigned int i = dummyVector.size()-1; i > 0; i-- ) {
    for(unsigned int j = i; j < dummyVector.size(); j++ ) {
      // calculate( dummyVector[i], dummyVector[j] ); 
    }
  }

答案 1 :(得分:1)

我建议看另一个答案(使用omp parallel的schedule指令)。

还有另一种选择,有可能计算一个线性索引,然后从中提取i和j,如下所示:

如果您必须同时计算i-&gt; j和j-&gt; i interations:

#pragma omp parallel for
for(size_t u=0; u<vector.size()*vector.size(); ++u) {
   size_t i = u/vector.size();
   size_t j = u%vector.size();
   if(i != j) {
      compute interaction between vector[i] and vector[j]
   }
}

如果互动是对称的(例如您的情况):

有一个类似的公式,但它更难。您需要生成以下(i,j)对的序列:

(1,0)(2,0)(2,1)(3,0)(3,1)(3,2)(4,0)(4,1)(4,2)(4) ,3)......

对于索引i,相关的耦合序列(i,j)的长度为i,因此将一对(i,j)转换为线性索引u的公式为:

u = i(i-1)/ 2 + j

现在需要反转&#39;这个公式,并检索整数i和j

当j = 0时,我会检索i的值:

i ^ 2 - i - 2 * u = 0

求解二次方程给出:

i =(1 +(int)(sqrt(1 + 8 * u)))/ 2

一个人推断出j的值:

j = u - i *(i-1)/ 2

是的,非常复杂的做法,我绝对更喜欢其他提议的解决方案,它不易出错!

edit1:声明u,i,j为size_t(而不是int)以避免溢出(如果vector.size()大于2次幂32仍然会发生,但这会留下合理的空间) 。感谢EOF的评论。

edit2:如果互动是对称的,那么它比我想象的更微妙(我的初始提案相当于load-inbalanced嵌套循环,请参阅注释)。

edit3:反转了对称交互案例的公式