首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >OpenMP代码比串行内存或线程开销瓶颈慢得多?

OpenMP代码比串行内存或线程开销瓶颈慢得多?
EN

Stack Overflow用户
提问于 2015-08-11 16:40:23
回答 4查看 1.7K关注 0票数 6

我试图并行化(OpenMP)一些科学的C++代码,其中大量(>95%)的CPU时间用于计算N~200阶不同粒子的讨厌(且不可避免的) O(N^2)交互作用。此计算重复1e10个时间步骤。我在OpenMP中尝试了各种不同的配置,每种配置都比串行代码慢一些(至少数量级),并且随着附加内核的增加而扩展得很差。

下面是相关代码的草图,具有代表性的虚拟数据层次结构Tree->Branch->Leaf。每个Leaf对象为当前和前三个时间步骤存储自己的位置和速度,以及其他内容。然后,每个Branch存储Leaf对象的集合,每个Tree存储Branch对象的集合。这种数据结构对于复杂但较少CPU密集型的计算非常有效,这些计算也必须在每个时间步骤(已经花了几个月才完成)执行。

代码语言:javascript
复制
#include <omp.h>

#pragma omp parallel num_threads(16) // also tried 2, 4 etc - little difference - hoping that placing this line here spawns the thread pool at the onset rather than at every step
{
while(i < t){
    #pragma omp master
    {
       /* do other calculations on single core, output etc.  */
       Tree.PreProcessing() 
       /* PreProcessing can drastically change data for certain conditions, but only at 3 or 4 of the 1e10 time steps */
       Tree.Output()
    }
    #pragma omp barrier
    #pragma omp for schedule(static) nowait
    for(int k=0; k < size; k++){
         /* do O(N^2) calc that requires position of all other leaves */
         Tree.CalculateInteraction(Branch[k]) 
    }
    /* return to single core to finish time step */
    #pragma omp master
    {
        /* iterate forwards */
        Tree.PropagatePositions()
        i++
    }
    #pragma omp barrier
}

简单地说,CPU-hog功能就是这样做的:

代码语言:javascript
复制
void Tree::CalculateInteraction(Leaf* A){
// for all branches B in tree{
       // for all leaves Q in B{
          if(condition between A and Q){skip}
          else{
                // find displacement D of A and Q 
                // find displacement L of A and "A-1"
                // take cross product of the two displacements
                // add the cross-product to the velocity of leaf A 
                for(int j(0); j!=3; j++){
                    A->Vel[j] += constant * (D_cross_L)[j];
                }

我的问题是,这种严重的性能问题是由于openMP线程管理开销占主导地位,还是由于设计的数据层次结构没有考虑并行性?

我应该注意到,每一步的时间都要比串行时间长得多,这不是初始化开销的问题;这两个版本已经测试了需要花费1vs10小时的计算,并最终希望应用于可能需要30小时的串行计算(对于这些计算,即使提高2倍的速度也是非常有益的)。而且,我在-fopenmp -march=native -m64 -mfpmath=sse -Ofast -funroll-loops中使用的是-fopenmp -march=native -m64 -mfpmath=sse -Ofast -funroll-loops 5.2.0,这可能是值得的。

我是OpenMP的新手,所以任何技巧都会非常感谢,如果有什么需要澄清的话,请告诉我。

EN

回答 4

Stack Overflow用户

回答已采纳

发布于 2015-08-17 15:18:01

感谢您提供到原始源的链接!我已经能够在两个平台上编译和获取一些统计数据:带有icpc 15.0和g++ 4.9.0的XeonE5-2670;以及带有g++ 4.8.4的Corei7-4770。

在Xeon上,icpc和g++都生成了随线程数缩放的代码。我运行了一个从发行版中的run.in文件派生出来的缩短的模拟(3e-7秒):

代码语言:javascript
复制
Xeon E5-2670 / icpc 15.0
threads   time   ipc
---------------------
1         17.5   2.17
2         13.0   1.53
4          6.81  1.53
8          3.81  1.52

Xeon E5-2670 / g++ 4.9.0
threads   time   ipc
---------------------
1         13.2   1.75
2          9.38  1.28
4          5.09  1.27
8          3.07  1.25

在核心i7上,我确实看到了您观察到的丑陋的缩放行为,使用g++ 4.8.4:

代码语言:javascript
复制
Core i7-4770 / g++ 4.8.4
threads   time   ipc
---------------------
1          8.48  2.41
2         11.5   0.97
4         12.6   0.73

第一个观察是,存在特定于平台的因素影响缩放。

我查看了point.hvelnl.cpp文件,注意到您正在使用vector<double>变量存储三维矢量数据,包括许多临时变量。这些都将访问堆,并且是一个潜在的争用源。Intel的openmp实现使用线程本地堆来避免堆争用,也许g++ 4.9也是这样,而g++-4.8.4没有吗?

我对这个项目(github上的halfflat/vfmcppar)进行了分叉,并修改了这些文件,使其对这些三维向量使用std::array<double,3>;这将恢复缩放,并提供更快的运行时间:

代码语言:javascript
复制
Core i7-4770 / g++ 4.8.4
std::array implementation
threads   time   ipc
---------------------
1          1.40  1.54
2          0.84  1.35
4          0.60  1.11

我还没有在相当长的模拟上运行这些测试,因此由于设置和i/o开销,很可能会丢失一些扩展。

要点是,任何共享资源都会阻碍可伸缩性,包括堆。

票数 3
EN

Stack Overflow用户

发布于 2015-08-12 18:56:29

您的问题很可能是错误共享,因为您对节点使用了链接列表。使用该内存布局,您不仅在每次将树遍历到另一个节点(如halfflat所述)时都会遇到缓存丢失的问题。

一个更严重的问题是,从不同线程访问和修改的树节点实际上可能在内存中关闭。如果它们共享缓存行,这意味着错误共享(或缓存ping-pong)会导致不同线程之间共享的缓存线的重复同步。

解决这两个问题的办法是避免链接数据结构。它们几乎总是造成低效率的原因。在您的示例中,解决方案是首先使用最少的数据(只需要定义树所需的数据)构建链接列表树,然后将其映射到另一棵不使用链接列表并可能包含更多数据的树。这就是我所做的,树遍历也是相当快的(树遍历永远不可能真的快,因为缓存丢失是不可避免的,即使是在相邻的姐妹节点上,因为父子访问不能同时是连续的)。如果按照旧树的顺序将粒子添加到新树中(这样可以避免缓存丢失),则可以为树的构建获得显著的速度(factor>2)。

票数 2
EN

Stack Overflow用户

发布于 2015-08-11 17:03:40

性能测量工具(如Linux )可能会为您提供一些关于缓存性能或争用的信息;优化的第一步是度量!

尽管如此,我的猜测是,这是一个数据布局问题,再加上速度更新的实现:在任何给定时间,每个线程都试图加载与(本质上)随机叶相关联的数据,这是缓存破坏的一种方法。与叶子相关的数据有多大,它们是否被安排在内存中相邻?

如果它确实是一个缓存问题(做度量!)然后,可以通过平铺N^2问题来解决这一问题:与其累积所有其他叶子所贡献的速度增量,不如将它们分批累积起来。考虑将N个叶子分割成K个批,以便进行此计算,其中每一批叶数据都符合(例如)您的缓存的一半。然后迭代批次的K^2对(A,B),执行交互步骤,即计算批B中所有叶子对批A中叶子的贡献,这应该可以在A中的叶子上并行完成,而不需要破坏缓存。

进一步的收益可以通过确保叶子被安排成连续的记忆批处理。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/31947412

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档