拥有正确且可执行的 OpenMP 程序之后,应该考虑其整体性能。您可以利用一些常规技术和 Sun 平台专有技术来改善 OpenMP 应用程序的效率和可伸缩性。我们将在此进行简单地介绍。
有关其他信息,请参见由 Rajat Garg 和 Ilya Sharapov 合著的《Techniques for Optimizing Applications: High Performance Computing》,该书可从 http://www.sun.com/books/catalog/garg.xml 上获得。
此外,有关 OpenMP 应用程序的性能分析和优化方面的参考文章和案例研究,请访问 Sun 开发人员门户网站,网址是 http://developers.sun.com/prodtech/cc/。
以下技术是用于改善 OpenMP 应用程序性能的一些常规技术。
将同步降至最低。
避免或尽量不使用 BARRIER、CRITICAL 段、ORDERED 区域和锁定。
使用 NOWAIT 子句可以消除冗余或不必要的屏障。例如,在并行区域末端总是有一个隐含的障碍。将 NOWAIT 添加到区域最后的 DO 中可以消除一个冗余的屏障。
使用已命名的 CRITICAL 段进行细粒度锁定。
使用显式 FLUSH 时要非常小心。刷新将造成数据高速缓存恢复到内存,而随后的数据访问可能需要从内存重新加载,从而会降低效率。
缺省情况下,空闲线程将在特定的超时期限后进入休眠状态。缺省超时期限对于您的应用程序而言可能过短,从而导致线程过早或过晚地进入休眠状态。可以使用 SUNW_MP_THR_IDLE 环境变量覆盖缺省超时期限,甚至可以使空闲线程永不进入休眠状态并始终处于活动状态。
尽可能在最高级别(如外部 DO/FOR 循环)执行并行化。在一个并行区域中封闭多个循环。通常,使并行区域尽可能大以降低并行化开销。例如:
此构造效率较低: !$OMP PARALLEL .... !$OMP DO .... !$OMP END DO .... !$OMP END PARALLEL !$OMP PARALLEL .... !$OMP DO .... !$OMP END DO .... !$OMP END PARALLEL 此构造效率较高: !$OMP PARALLEL .... !$OMP DO .... !$OMP END DO ..... !$OMP DO .... !$OMP END DO !$OMP END PARALLEL |
在并行区域中使用 PARALLEL DO/FOR 指令,而不是工作共享 DO/FOR 指令。与可能包含几个循环的常规并行区域相比,可以更有效地实现 PARALLEL DO/FOR。例如:
此构造效率较低: !$OMP PARALLEL !$OMP DO ..... !$OMP END DO !$OMP END PARALLEL 此构造效率较高: !$OMP PARALLEL DO .... !$OMP END PARALLEL |
在 Solaris 系统中,使用 SUNW_MP_PROCBIND 将线程绑定到处理器。处理器绑定与静态调度一起使用时,将有益于展示某个数据重用模式的应用程序,在该应用程序中,由并行区域中的线程访问的数据将位于上一次所调用并行区域的本地缓存中。请参见2.4 Solaris 上的处理器绑定。
尽可能使用 MASTER,而不是 SINGLE。
MASTER 指令作为不带隐式 BARRIER 的 IF 语句来实现: IF(omp_get_thread_num() == 0) {...}
SINGLE 指令的实现方式类似于其他工作共享构造。跟踪哪个线程首先到达 SINGLE 会增加额外的运行时开销。如果未指定 NOWAIT,则存在一个隐式 BARRIER。这样效率较低。
选择适当的循环调度。
STATIC 不会造成同步开销,并且可以在数据装入高速缓存时保持数据的局域性。但是,STATIC 可能导致负载失衡。
由于 DYNAMIC 和 GUIDED 要跟踪已经分配了哪些块,因此会发生同步开销。虽然这些调度会导致数据局域性较差,但是可以改善负载平衡。使用不同的块大小进行实验。
使用 LASTPRIVATE 时要非常小心,因为它有可能导致很高的开销。
从并行构造返回时,数据需要从专用区复制到共享存储区。
编译器代码检查哪个线程逻辑上执行最后一个迭代。这会在并行 DO/FOR 中每个块的末尾添加额外的工作。如果块数很多,开销将会增加。
使用有效的线程安全内存管理。
应用程序可以在编译器生成的代码中显式或隐式使用 malloc() 和 free(),以支持动态/可分配数组、向量化内例程等。
libc 中的线程安全 malloc() 和 free() 具有由内部锁定造成的高同步开销。可以在 libmtmalloc 库中找到更快的版本。请用 -lmtmalloc 进行链接以使用 libmtmalloc。
在数据量较少的情况下,OpenMP 并行循环可能性能不佳。使用 PARALLEL 构造中的 IF 子句,以指示循环仅应在那些可以预期提高某些性能的情况下运行并行。
如果可能,请合并循环。例如:
将以下两个循环
!$omp parallel do do i = ... |
statements_1
end do !$omp parallel do do i = ... |
statements_2
end do |
合并为一个循环
!$omp parallel do do i = ... |
statements_1
statements_2
end do |
如果应用程序缺乏超出某个级别的可伸缩性,请尝试使用嵌套并行操作。有关 OpenMP 中嵌套并行操作的更多信息,请参见1.2 本章所使用的特殊惯例。
如果不慎将共享内存结构与 OpenMP 应用程序一起使用,可能导致性能下降且可伸缩性受限制。多个处理器更新内存中相邻共享数据将导致多处理器互连的通信过多,因而造成计算序列化。
大多数高性能处理器(如 UltraSPARC 处理器)在 CPU 的低速内存和高速寄存器之间插入一个高速缓存缓冲区。访问内存位置时,会使包含所请求内存位置的一部分实际内存(缓存代码行)被复制到高速缓存中。随后可能在高速缓存外即可满足对同一内存位置或其周围位置的引用,直至系统决定有必要保持高速缓存和内存之间的一致性。
然而,同时更新来自不同处理器的相同缓存代码行中的单个元素会使整个缓存代码行无效,即使这些更新在逻辑上是彼此独立的。每次对缓存代码行的单个元素进行更新时,都会将此代码行标记为无效。其他访问同一代码行中不同元素的处理器将看到该代码行已标记为无效。即使所访问的元素未被修改,也会强制它们从内存或其他位置获取该代码行的较新副本。这是因为基于缓存代码行保持缓存一致性,而不是针对单个元素的。因此,互连通信和开销方面都将有所增长。并且,正在进行缓存代码行更新的时候,禁止访问该代码行中的元素。
这种情况称为伪共享。如果此情况频繁发生,OpenMP 应用程序的性能和可伸缩性就会显著下降。
在出现以下所有情况时,伪共享会使性能下降。
由多个处理器修改共享数据。
多个处理器更新同一缓存代码行中的数据。
这种更新发生的频率非常高(例如,在紧凑循环中)。
请注意,在循环中只读状态的共享数据不会导致伪共享。
在执行应用程序时,对占据主导地位的并行循环进行仔细分析即可揭示伪共享造成的性能可伸缩性问题。通常可以通过以下方式减少伪共享
尽可能使用专用数据;
利用编译器的优化功能来消除内存负载和存储。
在特定情况下,如果处理较大的问题而共享较少,可能较难看到伪共享的影响。
处理伪共享的方法与特定应用程序紧密相关。在某些情况下,更改数据的分配方式可以减少伪共享。在其他情况下,通过更改迭代到线程的映射,为每个线程的每个块分配更多的工作(通过更改 chunksize 值),也可以减少伪共享。
从 Solaris 9 发行版开始,操作系统为 SunFireTM 系统提供了可伸缩性和较高的性能。在 Solaris 9 操作系统中引入了新的特性,这些特性无需进行硬件升级即可提高 OpenMP 程序性能,这些特性包括内存定位优化 (Memory Placement Optimization, MPO) 和多页大小支持 (Multiple Page Size Support , MPSS)。
MPO 使操作系统可以将页面分配到访问这些页面的处理器附近。SunFire E20K 和 SunFire E25K 系统在相同的 UniBoardTM 内(与不同的 UniBoard 之间相比)有不同的内存延时。称为初次接触的缺省 MPO 策略在包含第一次接触内存的处理器的 UniBoard 上分配内存。对于大部分数据访问是针对每个处理器(处于初次接触定位状态)的局部内存的应用程序,初次接触策略可以显著提高该应用程序的性能。与内存平均分布在系统上的随机内存定位策略相比,此策略即可以降低应用程序的内存延迟,又能提高带宽,从而获得更高的性能。
从 Solaris 9 操作系统发行版开始,系统支持 MPSS 功能,且程序可以利用该功能对虚拟内存的不同区域使用不同的页面大小。缺省 Solaris 页面大小相对较小(在 UltraSPARC 处理器上为 8KB,在 AMD64 Opteron 处理器上为 4KB)。通过使用较大的页面大小,过多发生 TLB 未命中情况的应用程序可以提高性能。
使用 Sun 性能分析器可以测量 TLB 未命中次数。
使用以下 Solaris 操作系统命令可以获取特定平台的缺省页面大小:/usr/bin/pagesize。在此命令中使用 -a 选项可列出所有受支持的页面大小。(有关详细信息,请参见 pagesize(1) 手册页。)
有三种方法可更改应用程序的缺省页面大小:
使用 Solaris 操作系统命令 ppgsz(1)
使用 -xpagesize、-xpagesize_heap 和 -xpagesize_stack 选项编译应用程序。(有关详细信息,请参见编译器手册页。)
使用 MPSS 特有的环境变量。有关详细信息,请参见 mpss.so.1(1) 手册页。