OpenMP和OOP(分子动力学模拟)

时间:2012-12-19 18:35:35

标签: c++ parallel-processing openmp

我正在进行分子动力学模拟,并且我已经挣扎了很长一段时间并行实现它,虽然我成功地完全加载了我的4线程处理器,但并行的计算时间大于串行模式下的计算时间。

研究每个线程在哪个时间点开始并完成其循环迭代,我注意到了一种模式:就好像不同的线程正在等待彼此。 就在那时,我把注意力转向了程序的结构。我有一个类,其实例代表我的粒子系统,包含有关粒子的所有信息和使用此信息的一些函数。我还有一个类实例,它代表我的原子间势,包含潜在函数的参数以及一些函数(其中一个函数计算两个给定粒子之间的力)。

因此在我的程序中存在两个不同类的实例,它们彼此交互:一个类的一些函数引用另一个类的实例。 我试图并行实现的块看起来像这样:

      void Run_simulation(Class_system &system, Class_potential &potential, some other arguments){
          #pragma omp parallel for
              for(…) 
      }

for(...)是实际计算,使用来自system类的Class_system实例的数据和来自potential Class_potential实例的一些函数类。

我是对的,这个结构是我麻烦的根源吗?

你能否告诉我在这种情况下必须做些什么?我必须以完全不同的方式重写我的程序吗?我应该使用一些不同的工具来并行实现我的程序吗?

1 个答案:

答案 0 :(得分:5)

如果没有关于模拟类型的更多详细信息,我只能推测,所以这是我的推测。

您是否研究过负载均衡问题?我猜这个循环会在线程之间分配粒子,但如果你有某种限制范围的潜力,那么根据空间密度,模拟体积的不同区域的计算时间可能因粒子而异。这是分子动力学中非常常见的问题,并且在分布式存储器(大多数情况下为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时,计算ji施加的力会自动给你{{1}的力} {} {} {}} {} {} {} {} {} {} {} {}}但是i可能属于另一个可能同时修改它的线程,因此必须对两个更新使用原子操作(两个,如果它发生在邻居上,则另一个线程可能会更新j更多自己的粒子)。 x86上的原子更新使用锁定指令实现。这并不像经常出现的那么慢,但仍比常规更新慢。它还包括与错误共享相同的缓存行无效效果。为了解决这个问题,以增加内存使用为代价,可以使用本地数组来存储部分力贡献,然后在最后执行减少。减少本身必须与锁定指令串行或并行执行,因此可能会发现不仅使用这种方法没有增益,而且它甚至可能更慢。在处理元件之间进行适当的颗粒分选和巧妙分配,以最大限度地减少界面区域,可以用来解决这个问题。

我想要触及的另一件事是内存带宽。根据您的算法,在循环的每次迭代中获取的数据元素的数量与执行的浮点运算的数量之间存在一定的比率。每个处理器只有可用于内存提取的有限带宽,如果发生数据不完全适合CPU缓存,则可能发生内存总线无法提供足够的数据来提供单个执行单个线程的线程插座。你的Core i3-2370M只有3 MiB的L3缓存,所以如果你明确保持每个粒子的位置,速度和力,你只能在L3缓存中存储大约43000个粒子,在L2缓存中存储大约3600个粒子(或大约1800个)每个超线程的粒子。)

最后一个是超线程。正如高性能马克已经指出的那样,超线程共享大量的核心机器。例如,在两个超线程之间只共享一个AVX矢量FPU引擎。如果您的代码没有矢量化,则会丢失处理器中可用的大量计算能力。如果您的代码是矢量化的,那么两个超线程将在争夺对AVX引擎的控制权时进入彼此的方式。超线程只有在能够通过将计算(在一个超线程中)与内存负载(在另一个超线程中)重叠来隐藏内存延迟时才有用。使用密集的数字代码,在执行内存加载/存储之前执行许多寄存器操作,超线程不会带来任何好处,并且您可以使用一半的线程更好地运行并明确地将它们绑定到不同的内核,以防止OS调度程序从运行它们作为超线程。 Windows上的调度程序在这方面特别愚蠢,请参阅here以获取示例咆哮。英特尔的OpenMP实现支持通过环境变量控制的各种绑定策略。 GNU的OpenMP实现也是如此。我不知道在Microsoft的OpenMP实现中有任何控制线程绑定的方法(a.k.a. affinity mask)。