本章探讨一些可以提高数值密集型 Fortran 程序性能的优化技术。正确使用算法、编译器选项、库例程和编码习惯可以显著增进性能。本章不讨论高速缓存、I/O、或系统环境调节。并行化问题在下章论述。
本章探讨的问题包括:
可以提高性能的编译器选项
利用运行时性能配置文件中的反馈信息进行编译
使用公用过程已优化的库例程
用于提高关键循环性能的编码策略
优化与性能调节这一主题非常复杂,无法在此面面俱到。但本章的讨论将使读者对于这些问题获得初步的有益认识。本章最后列出的书籍对这一主题进行了更加深入的全面论述。
优化与性能调节是一门艺术,它在很大程度上依赖于判断优化或调节哪些内容的能力。
正确选择编译器选项是提高性能的第一步。Sun 编译器提供了范围广泛的选项,这些选项会对目标代码产生影响。缺省情况下,编译命令行中不会显式声明任何选项,此时大多数选项均为关闭。要提高性能,必须显式选择这些选项。
缺省时,性能选项通常都是关闭的,因为大多数优化都会强制编译器对用户源代码作出假设。符合标准编码习惯并且没有引入潜在副作用的程序应该可以正确优化。但是,不遵循标准编码方法的程序,可能会与编译器的某些假设有冲突。虽然最终代码的运行速度有可能加快,但计算结果却可能是错误的。
建议习惯是:先关闭所有选项进行编译,验证计算结果是否正确和准确,然后用这些初始结果和性能配置文件作为基准。接着,按步骤继续进行-用其他选项重新编译并将执行结果和性能与基准相比较。如果数值结果有变化,则程序可能存在可疑代码,需要进行仔细分析,确定可疑代码位置并重新编写。
如果增加优化选项后性能没有明显提高甚至降低,则说明编码可能没有给编译器提供进一步提高性能的机会。那么,为获得更好的性能,下一步应在源代码级分析并重新构造程序。
下表列出的编译器选项为用户提供了在缺省编译之上提高程序性能的一整套策略。表中只列出了更加有效的编译器性能选项中的一些选项。更完整的列表见《Fortran 用户指南》。
表 9–1 一些有效性能选项
操作 |
选项 |
---|---|
同时使用优化选项组合 |
-fast |
将编译器优化级别设置为 n |
-On (-O = -O3) |
指定通用目标硬件 |
-xtarget=sys |
指定特殊指令集架构 |
-xarch=isa |
使用性能配置文件数据进行优化(使用 -O5) |
-xprofile=use |
按 n 值展开循环 |
-unroll=n |
允许简化和优化浮点 |
-fsimple=1|2 |
执行依赖性分析以优化循环 |
-depend |
执行过程间优化 |
-xipo |
这些选项中的某些选项会增加编译时间,因为它们会调用更深层的程序分析。当例程与其调用例程被一起收入文件中(而不是将每个例程分割到自己的文件中)时,一些选项工作效果最佳;这样做将允许进行全局分析。
该选项定义为其他选项的特殊选择集,它会随版本和编译器的不同而变化。另外,-fast 选用的某些选项可能不是在所有平台上都可用。使用 -dryrun 标志进行编译可查看 -fast 的扩展。
-fast 可为某些基准测试应用程序提供高性能。但是,对于您的应用程序,选项的特定选择可能是合适的,也可能是不合适的。使用 -fast 是编译应用程序以获得最佳性能的良好起点。但是,仍然可能需要进行其他调整。如果用 -fast 编译时程序不能正常运行,请仔细查看组成 -fast 的各个选项,只调用那些适用于您程序的选项,使程序正常运行。
另请注意,用 -fast 编译的程序对于一些数据集可能会表现出良好的性能和精确的结果,而对于另一些数据集则不然。对于那些依赖浮点运算的特殊属性的程序,请避免用 -fast 进行编译。
由于 -fast 选择的某些选项具有链接含义,因此,如果在不同的步骤中进行编译和链接,还请务必用 -fast 进行链接。
–fast 会选用以下选项:
–dalign
–depend
–fns
–fsimple=2
-ftrap=common
—fround=nearest(仅限 Solaris)
–libmil
–xtarget=native
–O5
–xlibmopt(仅限 Solaris)
-pad=local(仅限 SPARC)
-xvector=lib(仅限 SPARC)
-nofstore(仅限 x86)
-xregs=frameptr(仅限 x86)
-fast 为运用编译器的诸多强大优化能力提供了一条捷径。可以单独指定复合选项中的每一个,并且每一选项都可能具有副作用,对此应引起注意(有关论述见《Fortran 用户指南》)。另请注意,-fast 的确切展开形式可能会随各个编译器发行版本而发生改变。使用 -dryrun 进行编译会显示所有命令行标志的展开形式。
随 -fast 一起使用其他选项可进一步增加优化。例如:
f95 -fast -m64 ...
可对启用了 64 位的平台进行编译。
由于 -fast 会调用 -dalign、-fns 和 -fsimple=2,因此用 -fast 编译的程序会导致非标准浮点运算、非标准数据对齐以及非标准表达式求值顺序。对于大多数程序来说,这些选择可能是不合适的。
除非显式指定 -O 选项(或使用类似 -fast 的宏选项隐式指定),否则编译器不会执行任何优化。几乎在所有情况下,在编译时指定优化级别都会提高程序执行性能。另一方面,优化级别越高编译时间就越长,并有可能显著增加代码长度。
对于大多数情况,采用 -O3 级别可在性能增益、代码长度和编译时间之间取得良好的平衡。-O4 级别将同一源文件中所含例程调用的自动内联添加作为调用者例程,除此之外它还会做一些其他事情。(有关子程序调用内联的进一步信息,参见《Fortran 用户指南》。)
-O5 级别会增添更多积极主动的优化技术,这些技术在更低级别不适用。一般而言,仅对于那些构成程序计算强度最高部分并因此而具有较高性能提高余地的例程,才应为其指定 -O3 以上的级别。(将用不同优化级别编译的程序部分链接起来,不存在任何问题。)
使用 C$ PRAGMA SUN OPT=n 指令可为一个源文件中的各个例程设置不同的优化级别。该指令将覆盖编译器命令行中的 -On 标志,但必须与 -xmaxopt=n 标志一起使用,才可设置最高优化级别。有关详细信息,参见 f95(1) 手册页。
当编译器在 O3 及更高级别应用其优化策略时,如果结合使用 -xprofile=use,将会大大提高效率。利用该选项,可以通过具有典型输入数据的程序(用 -xprofile=collect 编译)所产生的运行时执行配置文件来指导优化器。反馈配置文件会为编译器指出在哪里优化将会获得最大效果。这对于 -O5 选项可能尤为重要。下面给出了一个具有较高优化级别的配置文件集合的典型示例:
demo% f95 -o prg -fast -xprofile=collect prg.f ... demo% prg demo% f95 -o prgx -fast -O5 -xprofile=use:prg.profile prg.f ... demo% prgx |
例中的首次编译会生成一个在运行时产生语句覆盖统计的可执行文件。第二次编译使用该性能数据来指导程序的优化。
(有关 -xprofile 选项的详细信息,参见《Fortran 用户指南》。)
使用 -dalign,只要有可能,编译器就能生成双字加载/存储指令。用该选项编译后,执行大量数据操作的程序可能会显著受益。(它是 -fast 选用的选项之一。)双字指令的速度差不多是相应的单字操作的二倍。
但是,用户应注意,对于一些程序编码期待 COMMON 块中的数据按特定方式对齐的程序,使用 -dalign 选项(因此,对于 -fast 亦是如此)可能会带来问题。使用 -dalign,编译器可能会添加补白以确保所有双精度(和四精度)数据(REAL 或 COMPLEX)在双字边界对齐,结果会造成:
COMMON 块因添加了补白而有可能比预期的要大。
共享 COMMON 的程序单元,只要其中一个用 -dalign 编译,则必须全部用 -dalign 进行编译。
例如,如果某个程序是以单个数组形式通过别名使用具有混合数据类型的整个 COMMON 块来写入数据的,则该程序在 -dalign 下可能不会正常工作,原因是块比程序预期的要大(因双精度和四精度变量的补白所致)。
为 -O3 及更高的优化级别添加 -depend 可扩展编译器优化 DO 循环和循环嵌套的能力。使用该选项,优化器会分析迭代间的数据依赖性,以确定是否执行某一些循环结构转换。只能重构无数据依赖性的循环。但是,增添的分析可能会增加编译时间。
除非指示这样做,否则编译器不会尝试简化浮点计算(缺省设置为 -fsimple=0)。-fsimple=2 使优化器能够进行更富有成效的简化,同时要了解,由于舍入影响,这可能会导致一些程序产生稍稍不同的结果。如果使用 -fsimple 级别 1 或级别 2,所有程序单元应以类似方式进行编译以确保数值精度的一致性。有关该选项的重要信息,参见《Fortran 用户指南 》。
展开具有长迭代计数的短循环对某些例程很有利。但是,展开也会增加程序长度,甚至可能会降低其他循环的性能。如果 n=1(缺省值),优化器不会自动展开循环。如果 n 大于 1,优化器会尝试展开循环直至达到深度 n。
编译器的代码产生器根据因子个数决定是否展开循环。即使该选项是以 n>1 指定的,编译器也可能拒绝展开循环。
如果可以展开具有可变循环限制的 DO 循环,已展开的版本和原始循环均会被编译。对迭代计数进行的运行时测试将决定是否适合执行已展开的循环。循环展开,特别是对于只有一条或两条语句的简单循环,会增加每次迭代执行的计算量,并且会为优化器提供调度寄存器和简化操作的更好机会。迭代次数间的权衡、循环的复杂性以及展开深度的选择都不易确定,并且可能需要进行一些试验。
下例展示如何有可能用 -unroll=4 将简单循环展开成四级深度(源代码不会随该选项而改变):
原始循环:
DO I=1,20000 X(I) = X(I) + Y(I)*A(I) END DO |
经 4 次编译展开后,它会变成类似下面的样子:
DO I=1, 19997,4 TEMP1 = X(I) + Y(I)*A(I) TEMP2 = X(I+1) + Y(I+1)*A(I+1) TEMP3 = X(I+2) + Y(I+2)*A(I+2) X(I+3) = X(I+3) + Y(I+3)*A(I+3) X(I) = TEMP1 X(I+1) = TEMP2 X(I+2) = TEMP3 END DO |
本例展示了一个具有固定循环计数的简单循环。对于可变循环计数,重构将更加复杂。
如果编译器具有目标计算机硬件的精确描述,可能会提高一些程序的性能。当程序性能很重要时,目标硬件的正确说明会是非常重要的。在较新的 SPARC 处理器上运行时这一点尤其重要。但是,对于大多数程序和较早的 SPARC 处理器,性能增益是微不足道的,一般性说明可能就足够了。
《Fortran 用户指南》列出了 -xtarget= 识别的所有系统名称。对于任意给定的系统名称(例如,对于 UltraSPARC-II,名称为 ultra2),-xtarget 会扩展成与该系统正确匹配的 -xarch、-xcache 和 -xchip 的组合。优化器使用这些说明来确定遵循的策略和生成的指令。
特殊设置 -xtarget=native 使优化器能够针对主机系统(执行编译的系统)编译目标代码。当编译和执行均在相同的系统上进行时,这显然是非常有益的。当执行系统未知时,针对通用体系结构进行编译较为适宜。因此,缺省设置为 -xtarget=generic,即使它有可能达不到最佳性能。
-xtarget 和 -xchip 标志均接受 ultra3 和 ultra3 变体,并且为 UltraSPARC-III 和 UltraSPARC-IV 处理器生成优化代码。当在最新的 UltraSPARC 平台上编译和运行应用程序时,请指定 -fast 标志,以便为该平台自动选择正确的编译器优化选项。
对于交叉编译(编译在非最新的 UltraSPARC 平台上进行,但生成专用于在 UltraSPARC-III 处理器上运行的二进制代码),使用下列标志:
-fast -xtarget=ultra3
使用 -m64 编译可生成 64 位代码。
有关最新 UltraSPARC 处理器的 -xtarget 标志列表,请参见《Fortran 用户指南》。
在 UltraSPARC-III 和 UltraSPARC-IV 平台上进行性能剖析(使用 -xprofile=collect: 和 -xprofile=use:)效果尤为突出,这是因为它允许编译器识别经常执行的程序部分并进行最佳的本地化优化。
Sun Studio Fortran 编译器支持 Solaris 和 Linux x86 平台的 32 位和 64 位代码编译。
-xtarget=pentium3 标志将展开为:-xarch=sse -xchip=pentium3 -xcache=16/32/4:256/32/4.
对于 Pentium 4 系统,-xtarget=pentium4 将展开为:-xarch=sse2 -xchip=pentium4 -xcache=8/64/4:256/128/8.
新的 -m64 选项用来指定对 64 位 x64 指令集的编译。
新的 -xtarget 选项、-xtarget=opteron 为 32 位 AMD 编译指定了 -xarch、-xchip 和 -xcache 设置。
要生成 64 位代码,必须在命令行中 -fast 和 -xtarget 的后面指定 -m64。-xtarget 选项并不自动生成 64 位代码。-fast 选项也会产生 32 位代码,因为它也是一个定义 -xtarget 值的宏。所有当前 -xtarget 值(-xtarget=native64 和 -xtarget=generic64 除外)将产生 32 位代码,因此必须在 -fast 或 -xtarget 之后(右侧)指定 -xarch=m64 以编译 64 位代码,如下所示:
% f95 -fast -m64 或 % f95 -xtarget=opteron -m64
如果指定了 -xarch=amd64,编译器会立即预定义 __amd64 和 __x86_64。
在《Fortran 用户指南》中,可以找到有关 32 位和 64 位 x86 平台上的编译和性能的其他信息。
这个新的 f95 编译器标志是随 Forte Developer 6 update 2 发行版引入的,它通过调用过程间分析传递来执行整个程序优化。与 -xcrossfile 不同,-xipo 在链接步骤跨越所有目标文件进行优化,而不只限于编译命令中的源文件。
在编译和链接大型多文件应用程序时,-xipo 特别有用。用 -xipo 编译的目标文件内存有分析信息。这样便能够在源文件和预编译程序文件之上进行过程间分析。
有关如何有效使用过程间分析的详细信息,参见《Fortran 用户指南》。
在源代码的关键点处添加 ASSUME 指令,可以揭示用其他方法无法确定的重要程序信息,从而有助于指导编译器的优化策略。例如,可以告诉编译器 DO 循环的行程计数始终大于某个值,或某一 IF 分支很可能不会被执行。基于这些断言,编译器可以使用该信息生成更佳的代码。
作为一项附加的好处,通过启用运行时断言结果为假时的警告消息发布,程序员可以使用 ASSUME 编译指示来验证程序的执行。
有关详细信息,参见《Fortran 用户指南》第 2 章中的 ASSUME 编译指示介绍以及该手册第 3 章中的 -xassume_control 编译器命令行选项。
假设您已试验过使用各种优化选项,在编译完程序并测量了实际运行时性能之后,下一步可能就是要仔细观察 Fortran 源程序以确定可以进一步采取什么调节措施。
将注意力集中在那些占用计算时间最多的程序部分,考虑下列策略:
用等价的优化库替换手写过程。
从关键循环中删除 I/O、调用以及不必要的条件操作。
消除有可能抑制优化的别名使用。
合理化杂乱无章的代码以使用块 IF。
这些都是一些好的编程习惯,往往可以获得更佳的性能。可以更进一步,为特定硬件配置手动调节源代码。然而,这些尝试可能会进一步使代码变得含糊不清,甚至会使编译器的优化器更难获得显著的性能提高。过度手动调节源代码会掩藏过程的原始意图,并且会对不同体系结构的性能产生重大的有害影响。
在大多数情况下,经过优化的商业或共享件库执行标准计算过程远比采用手动编码方式更有效率。
例如,Sun Performance LibraryTM 是一套经过高度优化的数学子例程,它建立在标准 LAPACK、BLAS、FFTPACK、VFFTPACK 和 LINPACK 库的基础上。与手动编码相比,使用这些例程,性能有明显提高。有关详细信息,参见《Sun Performance Library User’s Guide》。
使用 Sun Studio 性能分析器可确定程序的关键计算部分。 然后仔细分析循环或循环嵌套,消除有可能抑制优化器生成优化代码或者不然会降低性能的编码。有许多影响可移植性的非标准编码习惯也可能会抑制编译器的优化。
本章最后所列的某些参考书籍更为详细地论述了用于提高性能的重编程技巧。有三种主要方法值得在此提出:
包含程序主要计算工作的循环或循环嵌套中的 I/O 会严重降低性能。花在 I/O 库上的 CPU 时间数量可能构成了循环所用时间的主要部分。(I/O 还会引起进程中断,因而降低程序处理能力。)尽可能将 I/O 移出计算循环,可以大大减少 I/O 库的调用次数。
循环嵌套深层调用的子程序可能会被调用数千次。即使每次调用花在每个例程上的时间很少,但累加效果却可能会很大。另外,由于编译器在调用期间不能对寄存器状态作出假设,所以子程序调用会抑制包含这些调用的循环的优化。
子程序调用的自动内联(使用 -inline=x,y,..z 或 -O4)是一种让编译器用子程序本身替换实际调用的方法(即将子程序拉到循环中)。要内联的例程的子程序源代码必须与调用例程存在于相同的文件中。
还有其他几种消除子程序调用的方法:
使用语句函数。如果正在调用的外部函数是一个简单的数学函数,可以将该函数改写为语句函数或语句函数集。语句函数采用内联编译,可以进行优化。
将循环推到子程序中。即改写子程序,减少其调用次数(循环外),并使其在每次调用时能对向量或数组值进行操作。
计算密集型循环内的复杂条件操作对编译器进行的优化尝试具有很强的抑制作用。一般而言,消除所有算术和逻辑 IF 操作而代之以块 IF 操作是一条很好的规则,应予以遵守:
Original Code: IF(A(I)-DELTA) 10,10,11 10 XA(I) = XB(I)*B(I,I) XY(I) = XA(I) - A(I) GOTO 13 11 XA(I) = Z(I) XY(I) = Z(I) IF(QZDATA.LT.0.) GOTO 12 ICNT = ICNT + 1 ROX(ICNT) = XA(I)-DELTA/2. 12 SUM = SUM + X(I) 13 SUM = SUM + XA(I) Untangled Code: IF(A(I).LE.DELTA) THEN XA(I) = XB(I)*B(I,I) XY(I) = XA(I) - A(I) ELSE XA(I) = Z(I) XY(I) = Z(I) IF(QZDATA.GE.0.) THEN ICNT = ICNT + 1 ROX(ICNT) = XA(I)-DELTA/2. ENDIF SUM = SUM + X(I) ENDIF SUM = SUM + XA(I) |
使用块 IF 不仅可以提高编译器生成优化代码的机会,而且可以增强可读性并确保可移植性。
如果用 -g 调试选项进行编译,可使用 er_src(1) 公用程序(Sun Studio 性能分析工具的一部分)来查看编译器生成的源代码注释。该公用程序还用来查看用所生成的汇编语言注释的源代码。下面例举了 er_src 对一个简单的 do 循环产生的注释:
demo% f95 -c -g -O4 do.f demo% er_src do.o Source file: /home/user21/do.f Object file: do.o Load Object: do.o 1. program do 2. common aa(100),bb(100) Function x inlined from source file do.f into the code for the following line Loop below pipelined with steady-state cycle count = 3 before unrolling Loop below unrolled 5 times Loop below has 2 loads, 1 stores, 0 prefetches, 1 FPadds, 1 FPmuls, and 0 FPdivs per iteration 3. call x(aa,bb,100) 4. end 5. subroutine x(a,b,n) 6. real a(n), b(n) 7. v = 5. 8. w = 10. Loop below pipelined with steady-state cycle count = 3 before unrolling Loop below unrolled 5 times Loop below has 2 loads, 1 stores, 0 prefetches, 1 FPadds, 1 FPmuls, and 0 FPdivs per iteration 9. do 1 i=1,n 10. 1 a(i) = a(i)+v*b(i) 11. return 12. end |
注释消息详细说明编译器所采取的优化操作。在例中可以看到:编译器内联了子例程调用并将循环展开了 5 次。仔细查看该信息可能会为进一步使用优化策略提供线索。
有关编译器注释和反汇编代码的详细信息,参见 Sun Studio《性能分析器》手册。
《High Performance Computing》,Kevin Dowd 和 Charles Severance 合著,O’Reilly & Associates,第二版,1998
《Techniques for Optimizing Applications: High Performance Computing》,Rajat Garg 和 Ilya Sharapov 合著,Sun Microsystems Press Blueprint,2001