我正在尝试并行化(OpenMP)一些科学C ++代码,其中CPU时间的大部分(> 95%)用于计算N阶N(N ^ 2)的令人讨厌(且不可避免)的O(N ^ 2)交互~200种不同的颗粒。该计算重复1e10个时间步长。我已尝试使用OpenMP进行各种不同的配置,每个配置比串行代码慢一些(至少数量级),并且随着附加内核的增加而缩放不良。
下面是相关代码的草图,具有代表性的虚拟数据层次结构Tree->Branch->Leaf
。每个Leaf
对象存储其自身的位置和速度,用于当前和之前的三个时间步骤等。然后,每个Branch
存储Leaf
个对象的集合,每个Tree
存储Branch
个对象的集合。这种数据结构非常适用于复杂但CPU密集度较低的计算,这些计算也必须在每个时间步骤执行(需要数月才能完善)。
#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功能可以做到这一点:
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线程管理开销占主导地位,或者是否是一个没有考虑并行性而设计的数据层次结构的情况?
我应该注意,每个步骤的时间要比串行时长得多,这不是一些初始化开销问题;两个版本已经过测试,计算时间为1对10小时,最终希望应用于可能需要30个小时的连续计算(对于这些计算,加速甚至加速2倍将是非常有益的)。此外,可能值得知道我正在使用带有-fopenmp -march=native -m64 -mfpmath=sse -Ofast -funroll-loops
的g ++ 5.2.0。
我是OpenMP的新手,所以我们非常感谢任何提示,如果有任何明白的话,请告诉我。
答案 0 :(得分:2)
由于您对节点使用链接列表,您的问题很可能是错误共享。使用该内存布局,您不仅几乎每次将树移动到另一个节点时都会出现缓存未命中的问题(如halfflat所述)。
更严重的问题是从不同线程访问和修改的树节点实际上可能在内存中关闭。如果他们共享一个缓存行,这意味着 false sharing (或 cache ping-pong )会导致重复同步不同线程之间共享的缓存行。
这两个问题的解决方案是避免链接数据结构。它们几乎总是效率低下的原因。在您的情况下,解决方案是首先使用最少的数据构建链接列表树(仅限定义树所需的数据),然后将其映射到不使用链接列表且可能包含更多数据的另一个树。这就是我所做的,并且树遍历相当快(树行走永远不会非常快,因为即使对于连续的姐妹节点,高速缓存未命中也是不可避免的,因为父女访问不能同时连续)。如果按照旧树的顺序将粒子添加到新树(这可以避免缓存未命中),则可以为树构建获得显着的加速(因子> 2)。
答案 1 :(得分:2)
感谢您提供原始来源的链接!我已经能够在两个平台上编译并得到一些统计数据:一个带有icpc 15.0和g ++ 4.9.0的Xeon E5-2670;在Core i7-4770上,使用g ++ 4.8.4。
在Xeon上,icpc和g ++都生成了与线程数一致的代码。我运行了一个缩短的(3e-7秒)模拟,该模拟源自分发中的run.in文件:
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
在Core i7上,我确实看到了你观察到的丑陋缩放行为,使用g ++ 4.8.4:
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.h
和velnl.cpp
文件,发现您使用vector<double>
变量来存储三维矢量数据,包括许多临时数据。这些都将访问堆,并且是潜在的争用来源。英特尔的openmp实现使用线程局部堆来避免堆争用,也许g ++ 4.9也可以,而g ++ - 4.8.4则没有?
我在github上分叉项目(halfflat/vfmcppar
)并修改这些文件以使用std::array<double,3>
来获取这些3-d向量;这可以恢复缩放,并且还可以提供更快的运行时间:
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开销,一些扩展很可能会丢失。
外卖点是任何共享资源都会阻碍可伸缩性,包括堆。
答案 2 :(得分:1)
性能测量工具(如Linux perf)可能会为您提供有关缓存性能或争用的一些信息;优化的第一步是测量!
那说,我的猜测是这是一个数据布局问题加上速度更新的实现:在任何给定时间的每个线程都试图加载与(基本上)随机叶子相关的数据,这是一个配方缓存颠簸。与叶子相关的数据有多大,它们是否被安排在内存中相邻?
如果它确实是一个缓存问题(做测量!)那么可以通过平铺N ^ 2问题来解决它:不是累积由所有其他叶子贡献的速度增量,它们可以批量累积。考虑将N个叶子分成K个批次以进行此计算,其中每批叶子数据适合(例如)缓存的一半。然后迭代批次的K ^ 2对(A,B),执行交互步骤,即计算批次B中所有叶子对批次A中叶子的贡献,这应该能够并行完成超过A中的树叶而不会破坏缓存。
通过确保叶子连续分批排列在存储器中,可以获得进一步的收益。
答案 3 :(得分:1)
可能与性能无关,但现在编写的代码具有奇怪的并行化结构。
我怀疑它可以产生正确的结果,因为while
内的parallel
循环没有障碍(omp master
没有障碍,omp for nowait
也没有障碍屏障)。
因此,(1)线程可能在主线程完成omp for
之前启动Tree.PreProcessing()
循环,一些线程实际上可能在主机工作单个之前执行omp for
任意次数预处理步骤; (2)master可以在其他线程完成Tree.PropagatePositions()
之前运行omp for
; (3)不同的线程可以运行不同的时间步长; (4)从理论上讲,主线程可以在某个线程进入并行区域之前完成while
循环的所有步骤,因此omp for
循环的某些迭代可能永远不会被执行。
或者我错过了什么?