Den*_*nis 4 c++ parallel-processing openmp
我正在进行分子动力学模拟,并且我一直在努力并行实现它,虽然我成功地完全加载了我的4线程处理器,但并行计算时间大于计算时间.串行模式.
研究每个线程在哪个时间点开始并完成其循环迭代,我注意到一个模式:就好像不同的线程正在等待彼此.就在那时,我把注意力转向了程序的结构.我有一个类,其实例代表我的粒子系统,包含有关粒子的所有信息和使用此信息的一些函数.我还有一个类实例,它代表我的原子间势,包含潜在函数的参数以及一些函数(其中一个函数计算两个给定粒子之间的力).
因此在我的程序中存在两个不同类的实例,它们彼此交互:一个类的一些函数引用另一个类的实例.我试图并行实现的块看起来像这样:
void Run_simulation(Class_system &system, Class_potential &potential, some other arguments){
#pragma omp parallel for
for(…)
}
Run Code Online (Sandbox Code Playgroud)
对于(...)是实际的计算,使用从数据system中的实例Class_system类,并从一些功能potential的实例Class_potential类.
我是对的,这种结构是我烦恼的根源吗?
你能否告诉我在这种情况下需要做些什么?我必须以完全不同的方式重写我的程序吗?我应该使用一些不同的工具来并行实现我的程序吗?
如果没有关于模拟类型的进一步细节,我只能推测,所以这是我的推测.
您是否研究过负载均衡问题?我猜这个循环会在线程之间分配粒子,但是如果你有某种限制范围的潜力,那么根据空间密度,模拟体积的不同区域的计算时间可能因粒子而异.这是分子动力学中非常常见的问题,并且在分布式存储器(大多数情况下为MPI)代码中很难正确解决.幸运的是,使用OpenMP,您可以直接访问每个计算元素上的所有粒子,因此负载平衡更容易实现.它不仅更容易,而且它也是内置的,可以这么说 - 只需for用schedule(dynamic,chunk)子句更改指令的调度,其中chunk是一个小数字,其最佳值可能因仿真而异.您可能使chunk输入数据的部分程序,或者你可以写schedule(runtime),然后通过设置不同的调度类玩OMP_SCHEDULE似的环境变量的值"static","dynamic,1","dynamic,10","guided",等.
另一个可能的性能下降源是错误共享和真正共享.当您的数据结构不适合并发修改时,会发生错误共享.例如,如果你保留每个粒子的3D位置和速度信息(假设你使用速度Verlet积分器),给定IEEE 754双精度,每个坐标/速度三元组需要24个字节.这意味着64字节的单个高速缓存行可容纳2个完整的三元组和2/3的另一个.这样做的结果是,无论你如何在线程中分配粒子,总会有至少两个线程必须共享一个缓存线.假设这些线程在不同的物理核心上运行.如果一个线程写入其缓存行的副本(例如,它更新粒子的位置),则将涉及缓存一致性协议,并且它将使另一个线程中的缓存行无效,然后必须从其重新读取它甚至来自主内存的较慢缓存.当第二个线程更新其粒子时,这将使第一个核心中的缓存行无效.解决此问题的方法是使用适当的填充和适当的块大小选择,这样就不会有两个线程共享一个缓存行.例如,如果添加表面的第4维(可以使用它来存储粒子在位置矢量的第4个元素中的势能以及速度矢量的第4个元素中的动能)然后每个位置/速度四元组将占用32个字节,并且恰好两个粒子的信息将适合单个高速缓存行.如果然后每个线程分配偶数个粒子,则会自动消除可能的错误共享.
当线程同时访问相同的数据结构并且结构的各个部分之间存在重叠(由不同的线程修改)时,会发生真正的共享.在分子动力学模拟中,这种情况经常发生,因为我们想要利用牛顿第三定律来在处理成对相互作用势时将计算时间减少到两个.当一个线程计算作用于粒子的力时i,在枚举其邻居时j,计算自动j施加i的力会给你i施加的力,j这样就可以将贡献加到总力上j.但是j可能属于可能同时修改它的另一个线程,因此原子操作必须用于两个更新(两者,i如果另一个线程碰巧与其自身的一个或多个粒子相邻,则它可能会更新).x86上的原子更新使用锁定指令实现.这并不像经常出现的那么慢,但仍比常规更新慢.它还包括与错误共享相同的缓存行无效效果.为了解决这个问题,以增加内存使用为代价,可以使用本地数组来存储部分力贡献,然后在最后执行减少.减少本身必须与锁定指令串行或并行执行,因此可能会发现不仅使用这种方法没有增益,而且它甚至可能更慢.可以使用适当的颗粒分选和处理元件之间的巧妙分配以最小化界面区域来解决该问题.
我想要触及的另一件事是内存带宽.根据您的算法,在循环的每次迭代中获取的数据元素的数量与执行的浮点运算的数量之间存在一定的比率.每个处理器只有可用于内存提取的有限带宽,如果发生数据不完全适合CPU缓存,则可能发生内存总线无法提供足够的数据来提供单个执行单个线程的线程插座.你的Core i3-2370M只有3 MiB的L3缓存,所以如果你明确保持每个粒子的位置,速度和力,你只能在L3缓存中存储大约43000个粒子,在L2缓存中存储大约3600个粒子(或大约1800个)每个超线程的粒子).
最后一个是超线程.正如高性能马克已经指出的那样,超线程共享大量的核心机器.例如,在两个超线程之间只共享一个AVX矢量FPU引擎.如果您的代码没有矢量化,则会丢失处理器中可用的大量计算能力.如果您的代码是矢量化的,那么两个超线程将在争夺对AVX引擎的控制权时进入彼此的方式.超线程只有在能够通过将计算(在一个超线程中)与内存负载(在另一个超线程中)重叠来隐藏内存延迟时才有用.使用密集的数字代码在执行内存加载/存储之前执行许多寄存器操作,超线程无论如何都没有任何好处,您可以使用一半的线程更好地运行并明确地将它们绑定到不同的内核,以防止OS调度程序运行他们作为超线程.Windows上的调度程序在这方面特别愚蠢,请参阅此处的示例咆哮.英特尔的OpenMP实现支持通过环境变量控制的各种绑定策略.GNU的OpenMP实现也是如此.我不知道在Microsoft的OpenMP实现中有任何方法来控制线程绑定(也就是亲和掩码).