我需要计算对象向量中所有元素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
增长时损害我的运行时。我能做得比这更好吗?
答案 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:反转了对称交互案例的公式