本节介绍了几项可以提高 OpenMP 应用程序性能的通用技术。
将同步降至最少。
避免使用或最大限度地少用同步功能,如 barrier、critical、ordered、taskwait 和锁。
使用 nowait 子句可以消除冗余或不必要的屏障。例如,在并行区域末端总是有一个隐含的障碍。如果区域中的工作共享循环后面不跟区域中的任何代码,可以向其添加 nowait,从而消除一个冗余屏障。
适用的时候,对细粒度的锁定使用命名的 critical 段,以免程序中的所有 critical 段都使用相同的缺省锁。
使用 OMP_WAIT_POLICY、SUNW_MP_THR_IDLE 或 SUNW_MP_WAIT_POLICY 环境变量控制等待线程的行为。缺省情况下,空闲线程将在特定的超时期限后进入休眠状态。如果线程在超时期限结束时未找到工作,则会进入休眠状态,从而避免以其他线程为代价浪费处理器周期。缺省超时期限对于您的应用程序可能不合适,从而导致线程过早或过晚地进入休眠状态。通常,如果应用程序在专用处理器上运行,使等待线程旋转的主动等待策略将会提供更高的性能。如果应用程序与其他应用程序同时运行,将等待线程置于休眠状态的被动等待策略将会改善系统吞吐量。
尽可能在最高级别(如最外面的循环)执行并行化。在一个并行区域中封闭多个循环。通常,使并行区域尽可能大以降低并行化开销。例如,以下构造效率不高:
#pragma omp parallel { #pragma omp for { ... } } #pragma omp parallel { #pragma omp for { ... } }
更有效的构造:
#pragma omp parallel { #pragma omp for { ... } #pragma omp for { ... } }
使用 parallel for/do 构造,而不是嵌套在 parallel 构造中的工作共享 for/do 构造。例如,以下构造效率不高:
#pragma omp parallel { #pragma omp for { ... statements ... } }
此构造更有效:
#pragma omp parallel for { ... statements ... }
如有可能,合并并行循环以避免并行开销。例如,合并两个 parallel for 循环:
#pragma omp parallel for for (i=1; i<N; i++) { ... statements 1 ... } #pragma omp parallel for for (i=1; i<N; i++) { ... statements 2 ... }
合并后的一个 parallel for 循环更高效:
#pragma omp parallel for for (i=1; i<N; i++) { ... statements 1 ... ... statements 2 ... }
使用 the OMP_PROC_BIND 或 SUNW_MP_PROCBIND 环境变量将线程绑定到处理器。处理器绑定与静态调度一起使用时,将有益于展示某个数据重用模式的应用程序,在该应用程序中,由并行区域中的线程访问的数据将位于上一次所调用并行区域的本地缓存中。请参见Chapter 5, 处理器绑定(线程关联性)。
尽可能使用 master,而不是 single。
master 指令作为不带隐式屏障的 if 语句实现:if (omp_get_thread_num() == 0) {...}
single 构造的实现方式类似于其他工作共享构造。跟踪哪个线程首先到达 single 会增加额外的运行时开销。此外,如果未指定 nowait,则存在一个隐式屏障,这样效率较低。
选择适当的循环调度。
static 循环调度不要求同步,在数据与高速缓存相符的情况下可保持数据局域性。但是,static 调度可能导致负载不平衡。
由于 dynamic 和 guided 循环调度要跟踪已经分配了哪些块,因此会发生同步开销。虽然这些调度会导致数据局域性较差,但是可以改善负载平衡。使用不同的块大小进行实验。
使用有效的线程安全内存管理。应用程序可以在编译器生成的代码中显式或隐式使用 malloc() 和 free() 函数,以支持动态数组、可分配数组、向量化内部函数等。标准 C 库 libc.so 中的线程安全 malloc() 和 free() 具有由内部锁定造成的高同步开销。可以在其他库(如 libmtmalloc.so 库)中找到更快的版本。指定 –lmtmalloc 与 libmtmalloc.so 链接。
如果数据集较小,可能会导致 OpenMP 并行区域性能不佳。在 parallel 构造中使用 if 子句指定区域仅应在预期可以提高一定性能的情况下并行运行。
如果应用程序缺乏超出某个级别的可伸缩性,请尝试使用嵌套并行操作。不过,使用嵌套并行操作时要非常谨慎,它会增加同步开销,因为每个嵌套并行区域的线程组均需在屏障处进行同步。另外,嵌套并行操作可能会占用过多的计算机资源,从而导致性能下降。
使用 lastprivate 时要非常谨慎,因为它有可能导致很高的开销。
在从区域返回之前,必须将数据从线程的专用内存复制到共享内存。
lastprivate 会增加额外的检查。例如,包含 lastprivate 子句的工作共享循环的编译代码会检查哪个线程执行顺序中的上一个迭代。这会在循环中每个块的末尾增加额外的工作,如果有许多个块,这些额外的工作还会累加。
使用显式 flush 时要非常谨慎。刷新将造成数据存储到内存,而随后的数据访问可能需要从内存重新加载,这些都会降低效率。