1

我在使用 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 个“命令”。

  1. 根据键获取地图迭代器。见行:node = vec_keys.at(itime);

  2. 使用第 1 步中的迭代器,获取指向 C++ 对象的指针,该对象将在其上调用方法。见行:p_nim = p_dmb->getModel(node);

  3. 在步骤 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 给出了相同的结果。

对于为什么这些线程在执行时间上有如此大的差异以及我能做些什么来消除它的任何帮助,我将非常感激。

4

1 回答 1

1

首先,您真的确定花费更长的线程做的工作更少吗?因为奇怪的是,那些只在少数项目上工作的线程总是花费最长的时间。如果是这种情况,您可以尝试考虑以下因素:

  • 是否存在错误共享(多个线程访问同一缓存行,至少其中一些线程在写入)?
  • 您提到它在 2 节点机器上工作。您可以尝试查看哪个节点执行哪些线程以及分配内存蜂的位置。除非节点之间的互连真的很糟糕,否则我怀疑这是问题所在。

虽然不是关于为什么某些线程较慢的答案,但您可能想要尝试guideddynamic调度循环(例如#pragma omp for nowait schedule(dynamic, 10000),您当然想要微调以chunk_size获得最大性能),通过使工作负载更均匀地分布在线程之间更快的线程需要更多的负载。

作为旁注:考虑到c ++允许在任何结构化块内声明变量,并且在并行部分内声明的变量无论如何都是线程私有的,为什么您需要所有这些私有变量。因此,在并行部分内首次使用时声明变量可能是提高可读性甚至性能的好主意(尽管在这种情况下不太可能)。

于 2012-08-20T20:46:14.367 回答