我在使用 OMP 线程时遇到问题,因为并行部分中的线程需要非常不同的时间来执行。我在 Linux 集群上运行。该代码是纯 OpenMP 代码;例如,没有混合 MPI 代码。它是用 C++ 编写的。我使用的是 gcc 4.5.3,因此使用的是 OpenMP 3.0。我正在使用编译优化级别 2;即,-O2。我先给出代码,然后给出从中生成的数据。由于我想循环遍历映射的键,我首先将键复制到向量 vec_keys 中,然后对 vec_keys 的元素运行并行 for 循环。有一个与 OMP 并行的 for 循环。必须处理 800,000 个“节点”或条目。并行 for 循环中有 3 个“命令”。
根据键获取地图迭代器。见行:node = vec_keys.at(itime);
使用第 1 步中的迭代器,获取指向 C++ 对象的指针,该对象将在其上调用方法。见行:p_nim = p_dmb->getModel(node);
在步骤 2 中从地图中检索到的对象上调用该方法。参见语句:
isStateUpdate = p_nim->computeNextState(day, iteration, fsmId, p_dmb, tid, node, p_np,p_ep,p_cp,p_fg,newStateNp, outStream);
请注意,在第 2 步中,地图条目被检索但不被写入。在第 3 步中,地图的内容正在发生变化,但通过间接方式进行。也就是说,映射键不会改变。这些值(在映射条目中)是指向在堆上实例化的原始数组的指针。因此,通过不改变值指针,我可以改变原始数组的内容。关键是我正在使用地图,每个键在 OMP for 循环中被调用一次,并且没有竞争条件或不一致的内存。我用 1、2、4 和 8 个线程运行了很多次,并且输出总是正确的。上面第 1 步和第 2 步的操作对于每个 map 键都是一样的;只有第 3 步可以不同。代码是:
#pragma omp parallel num_threads(numSpecOmpThreads) \
private(itime,node,index,modelId,isStateUpdate,tid, \
b1time, \
e1time) \
shared(numkeys,vec_keys,p_graph,p_cp,ifsm,p_np,p_dmb, \
p_ep,p_fg,newStateNp,day,iteration,fsmId, \
fakeMpiRankOpenMp,cout,outStream, \
startWtime,endWtime,counter, \
sinnertime,einnertime, \
dt1time, \
dt2time, \
dt3time, \
countChange) \
default(none)
{
// Any variable in here is private.
tid=omp_get_thread_num();
NodeInteractModel02IF *p_nim=0;
startWtime[tid] = omp_get_wtime();
if (tid==0) {
gettimeofday(&sinnertime,0);
}
#pragma omp for nowait
for (itime=0; itime<numkeys; ++itime) {
++(counter[tid]);
// node is a tail, or owned node.
// This is step 1.
gettimeofday(&b1time,0);
node = vec_keys.at(itime);
gettimeofday(&e1time,0);
dt1time[tid] += (1000000 * (e1time.tv_sec - b1time.tv_sec) +
e1time.tv_usec - b1time.tv_usec);
// This is step 2.
gettimeofday(&b1time,0);
p_nim = p_dmb->getModel(node);
gettimeofday(&e1time,0);
dt2time[tid] += (1000000 * (e1time.tv_sec - b1time.tv_sec) +
e1time.tv_usec - b1time.tv_usec);
// This is step 3.
gettimeofday(&b1time,0);
isStateUpdate = p_nim->computeNextState(lots of args);
gettimeofday(&e1time,0);
dt3time[tid] += (1000000 * (e1time.tv_sec - b1time.tv_sec) +
e1time.tv_usec - b1time.tv_usec);
if (isStateUpdate) {
++(countChange[tid]);
}
} // End FOR over vector of owned nodes.
endWtime[tid] = omp_get_wtime();
if (tid==0) {
gettimeofday(&einnertime,0);
}
} // End pragma on OMP parallel.
现在,问题。以8线程执行为例。执行结果如下所示。这些是典型的。dt1 是运行上述第一步的累积时间,以秒为单位;dt2 是运行上述第二步的累计时间;dt3 是运行上述步骤 3 的累积时间。cum=dt1+dt2+dt3。countChange 是步骤 3 中发生变化的“节点”数量的计数。有 8 行数据,每个线程一个(tid=0 是第一行数据,...,tid=7 是最后一行)。这次运行有 800,000 个“节点”,因此最多可以有 8 x 100,000 = 800,000 个 countChanges。我已经确认每个线程正在处理总共 800,000 个节点中的 100,000 个。因此,每个线程的工作——就要处理的节点数量而言——是相同的。但是,如下所述,
+++++++++++++++++++++++++++++++
dt1 dt2 dt3 cum (s) countChange
0.013292 0.041117 3.10149 3.1559 15
0.009705 0.041273 3.17969 3.23067 21
0.009907 0.040998 3.29188 3.34279 16
0.009905 0.040169 3.38807 3.43814 26
0.023467 0.039489 0.198228 0.261184 100000
0.023945 0.038114 0.187334 0.249393 100000
0.023648 0.042231 0.197294 0.263173 100000
0.021285 0.046682 0.219039 0.287006 100000
正如预期的那样,dt1 小于 dt2。正如预期的那样,两者都小于 dt3,因为步骤 3 涉及计算。但是请注意,dt3 值的问题:它们的变化超过一个数量级,并且它们分为两组:一组具有 dt3 ~ 3.2,一组具有 dt3 ~ 0.19。此外,执行速度最快的线程是执行最多的线程工作; 后四个线程中的每一个都更改所有 100,000 个值,而前四个线程在 15-26 个值之间更改(这显然是小于 100,000 的数量级)。后 4 个线程做更多的工作,因为当一个节点改变时有更多的计算。此外,我正在运行的机器是一个 2 节点、每节点 4 核的计算节点。我希望主线程是 tid=0 并且时间会更短(如果有的话),但它在时间更长的组中。此外,单线程代码产生 cum ~ 11.3 秒。现在,11.3/8 = 1.41 秒。
由于代码执行此循环以及其他类似循环数百万次,因此理想时间(1.41 秒)和最大测量时间(以上 3.44 秒)之间的差异是巨大的,而且似乎过大。
此外,如果使用 4 个线程而不是 8 个线程运行上述示例,则前两个线程的时间过多,后两个线程的时间快。参见以下 4 线程数据:
+++++++++++++++++++++++++++++++
dt1 dt2 dt3 cum (s) countChange
0.023794 0.073054 5.41201 5.50886 36
0.018677 0.072956 5.77536 5.86699 42
0.027368 0.066898 0.341455 0.435721 200000
0.026892 0.076005 0.363742 0.466639 200000
同样,前两个线程和后两个线程之间的差异在时间上是一个数量级(~5.5 vs ~0.4);再一次,运行最快的线程做的工作最多。
这是示例 2 线程数据。第二个线程做的工作更多——更改了 400,000 个节点,而第一个线程只更改了 78 个节点——但运行速度却快了一个数量级(10.8 对 0.8)。+++++++++++++++++++++++++++++++
dt1 dt2 dt3 cum (s) countChange
0.025298 0.144209 10.6269 10.7964 78
0.019307 0.126661 0.619432 0.7654 400000
我已经多次单独使用 OpenMP 以及在组合 OpenMP + MPI 代码上重复这个实验,并且每次都得到相同的结果(当然,这些值稍微调整了一下,但趋势相同)。前一半的线程(tid 最小的线程)运行时间最长且工作较少。此外,使用 gcc 4.7.0,因此 OpenMP 3.1 给出了相同的结果。
对于为什么这些线程在执行时间上有如此大的差异以及我能做些什么来消除它的任何帮助,我将非常感激。