C++和OpenMP——线程的执行时间大不相同

C++ and OpenMP--vastly different execution times for threads

本文关键字:执行时间 大不相同 线程 OpenMP C++      更新时间:2023-10-16

我在OMP线程处理方面遇到了一个问题,因为并行部分中的线程执行时间非常不同。我在Linux集群上运行。该代码是一个纯OpenMP代码;例如,没有混合MPI代码。它是用C++编写的。我使用的是gcc 4.5.3,因此是OpenMP 3.0。我使用的是编译优化级别2;即-O2。我先给出代码,然后给出从中生成的数据。由于我想在贴图的关键帧上循环,所以我首先将关键帧复制到向量vec_keys中,然后在vec_key的元素上运行并行for循环。存在一个与OMP并行的for循环。必须处理800000个"节点"或条目。并行for循环中有3个"命令"。

  1. 获取基于键的映射迭代器。请参见行:node=vec_keys.at(itime);

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

  3. 在步骤2中对从映射中检索到的对象调用该方法。参见语句:
    isStateUpdate=p_nim->computeNextState(天,迭代,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中的操作对于每个地图键都是相同的;只有步骤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是最后一行)。此运行中有800000个"节点",因此最多可以有8 x 100000=800000个countChanges。我已经确认,在总共800000个节点中,每个线程处理100000个节点。因此,就要处理的节点数量而言,每个线程的工作量是相同的。然而,如下所述,每个线程的计算量并不相同。

+++++++++++++++++++++++++++

dt1 dt2 dt3 cum(s)count更改

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。此外,执行速度最快的线程是执行最多工作的线程;后四个线程中的每一个都改变了所有100000个值,而前四个线程在15-26个值之间改变(这显然是小于100000的数量级)。后4个线程做更多的工作,因为当节点发生变化时,会有更多的计算。此外,我运行的机器是一个2节点、每节点4核的计算节点。我预计主线程将是tid=0,并且会有一个较低的时间(如果有的话),但它在时间较多的组中。此外,单线程代码产生cum~11.3秒。现在,11.3/8=1.41秒。

由于代码执行这个循环和其他类似的循环数百万次,理想时间(1.41秒)和最大测量时间(3.44s)之间的差异是巨大的,而且似乎过大。

此外,如果用4个线程而不是8个线程运行上述示例,则前两个线程具有过多的时间,而后两个线程的时间较快。请参阅以下4线程数据:

+++++++++++++++++++++++++++

dt1 dt2 dt3 cum(s)count更改

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对~0.4);同样,运行速度最快的线程所做的工作最多。

以下是示例2线程数据。第二个线程做了更多的工作——更改了400000个节点,而第一个线程只更改了78个节点——但运行速度快了一个数量级(10.8比0.8)。+++++++++++++++++++++++++++

dt1 dt2 dt3 cum(s)count更改

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也给出了相同的结果。

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

首先,你真的确定耗时更长的线程所做的工作更少吗?因为奇怪的是,那些只处理少数项目的线程总是花费最长的时间。如果是这样的话,你可以尝试考虑以下因素:

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

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

顺便说一句:考虑到c++允许在任何结构化块中声明变量,而在并行部分中声明的变量对线程来说无论如何都是私有的,你为什么需要所有这些私有变量呢。因此,在并行部分中首次使用时声明变量对于可读性甚至性能来说可能是个好主意(尽管在这种情况下不太可能)。