一般情况下,通过消除所有非标准编码,可以更简单地将一个应用程序从一个系统和编译器移植到另一个系统和编译器。在一个系统中大获成功的优化或解决方法,在其他系统中可能只会给编译器造成模糊和混乱。特别是,针对某一特定体系结构所做的优化手动调节,在别处可能会造成性能下降。对此将在后面关于性能和调节的章节中予以讨论。但是,就一般性移植而言,下列问题值得考虑。
有些系统会自动将局部变量和 COMMON 变量初始化为零或者“非数字”(NaN) 值。但是,没有任何标准做法,而且程序不应对任何变量的初始值进行假设。要确保最大程度的可移植性,程序应初始化所有变量。
当同一个存储地址被多个名称引用时,就会出现别名使用情况。这种情况通常发生在指针上,或者是在子程序的实际参数相互重叠或与子程序内的 COMMON 变量相互重叠时发生。例如,参数 X 和 Z 引用同一存储位置,B 和 H 亦然:
COMMON /INS/B(100) REAL S(100), T(100) ... CALL SUB(S,T,S,B,100) ... SUBROUTINE SUB(X,Y,Z,H,N) REAL X(N),Y(N),Z(N),H(N) COMMON /INS/B(100) ... |
作为一种手段,很多“旧式”Fortran 程序利用这种别名使用机制来提供当时程序语言中尚不具备的某种动态内存管理。
在所有可移植代码中避免别名使用。在某些平台上以及在用高于 -O2 的优化级别编译时,其结果可能是无法预料的。
f95 编译器会假定其编译的是符合标准的程序。不严格遵循 Fortran 标准的程序可能会引起二义性情况,从而干扰编译器的分析和优化策略。某些情况甚至能产生错误结果。
例如,数组索引越界、使用指针或将仍在直接使用的全局变量作为子程序参数传递,都可导致二义性情况,从而限制了编译器生成在所有情况下都正确的优化代码的能力。
如果知道程序的确包含一些明显的别名使用情况,可使用 -xalias 选项指定编译器的关注程度。在某些情况下,当以高于 -O2 的优化级别编译时,程序不会正确执行,除非指定了适当的 -xalias 选项。
此选项标志会获取一个以逗号分隔的、指示别名使用情况类型的关键字列表。可在每个关键字前冠以 no% 前缀,用以指示不存在的别名使用。
表 7–2 -xalias 关键字及其含义
这里列举了别名使用情况的一些典型示例。在较高优化级别(-O3 及其以上级别)上,如果您的程序中不存在如下所示的别名使用弊病,并且您是用 -xalias=no%keyword 进行编译的,f95 编译器可以生成更好的代码。
在某些情况下,您需要使用 -xalias=keyword 进行编译,以确保代码生成将会产生正确的结果。
下例需要使用 -xalias=dummy 进行编译
parameter (n=100) integer a(n) common /qq/z(n) call sub(a,a,z,n) ... subroutine sub(a,b,c,n) integer a(n), b(n) common /qq/z(n) a(2:n) = b(1:n-1) c(2:n) = z(1:n-1) 编译器必须假设伪变量和公用变量可以重叠。 |
本例仅在使用 -xalias=craypointer(此为缺省设置)编译时才适用:
parameter (n=20) integer a(n) integer v1(*), v2(*) pointer (p1,v1) pointer (p2,v2) p1 = loc(a) p2 = loc(a) a = (/ (i,i=1,n) /) ... v1(2:n) = v2(1:n-1) 编译器必须假设这些位置可以重叠。 |
下面给出了一个 Cray 指针不重叠的例子。此时,用 -xalias=no%craypointer 进行编译可能会获得更佳的性能:
parameter (n=10) integer a(n+n) integer v1(n), v2(n) pointer (p1,v1) pointer (p2,v2) p1 = loc(a(1)) p2 = loc(a(n+1)) ... v1(:) = v2(:) Cray 指针不指向重叠内存区。 |
用 -xalias=ftnpointer 编译以下示例
parameter (n=20) integer, pointer :: a(:) integer, target :: t(n) interface subroutine sub(a,b,n) integer, pointer :: a(:) integer, pointer :: b(:) end subroutine end interface a => t a = (/ (i, i=1,n) /) call sub(a,a,n) .... end subroutine sub(a,b,n) integer, pointer :: a(:) real, pointer :: b(:) integer i, mold forall (i=2:n) a(i) = transfer(b(i-1), mold) 编译器必须假设 a 和 b 可以重叠。 |
注意:在本例中,编译器必须假设 a 和 b 可以重叠,即使它们指向不同数据类型的数据。这在标准 Fortran 中是非法的。如果编译器能够检测到此种情况,它会发出警告。
用 -xalias=overindex 编译以下示例
integer a,z common // a(100),z z = 1 call sub(a) print*, z subroutine sub(x) integer x(10) x(101) = 2 编译器假设对 sub 的调用可以写入 z 用 -xalias=overindex 编译时,程序打印 2 而非 1 |
索引越界在很多传统 Fortran 77 程序中都会出现,应予以避免。在很多情况下,结果将无法预料。要确保正确性,应使用 -C(运行时数组边界检查)选项编译和测试程序,以标记任何数组下标问题。
一般而言,overindex 标志只能与传统 Fortran 77 程序一起使用。-xalias=overindex 不适用于数组语法表达式、数组段、WHERE 和 FORALL 语句。
为确保生成代码的正确性,Fortran 95 程序应该总是符合 Fortran 标准中的下标规则。例如,下例的一个数组语法表达式中使用了二义性下标,该表达式因数组索引越界将始终产生不正确的结果:
本例中数组语法索引越界不会产生正确的结果! parameter (n=10) integer a(n),b(n) common /qq/a,b integer c(n) integer m, k a = (/ (i,i=1,n) /) b = a c(1) = 1 c(2:n) = (/ (i,i=1,n-1) /) m = n k = n + n C C the reference to a is actually a reference into b C so this should really be b(2:n) = b(1:n-1) C a(m+2:k) = b(1:n-1) C or doing it in reverse a(k:m+2:-1) = b(n-1:1:-1) 从直观上,用户期望数组 b 现在与数组 c 相似,但结果是无法预料的 |
xalias=overindex 标志无助于此种情况,因为 overindex 标志没有扩展至数组语法表达式。此例虽然可以编译,但不会给出正确结果。用等价的 DO 循环替换数组语法,改写本例,在用 -xalias=overindex 进行编译后,就正常了。但应完全避免这种编程习惯。
编译器超前查看局部变量是如何使用的,然后假设变量不会随子程序调用而变化。在下例中,子程序中使用的指针使编译器优化策略失败,并且结果无法预料。要使此例正确工作,需用 -xalias=actual 标志进行编译:
program foo integer i call take_loc(i) i = 1 print * , i call use_loc() print * , i end subroutine take_loc(i) integer i common /loc_comm/ loc_i loc_i = loc(i) end subroutine take_loc subroutine use_loc() integer vi1 pointer (pi,vi) common /loc_comm/ loc_i pi = loc_i vi1 = 3 end subroutine use_loc |
take_loc 会获取 i 的地址,并将其保存起来。use_loc 将使用它。这违反了 Fortran 标准。
用 -xalias=actual 标志进行编译,将会通知编译器应将传给子程序的所有参数在编译单元内看作是全局性的,从而使编译器在对作为实际参数出现的变量作出假设时更加小心。
应避免诸如此类违反 Fortran 标准的编程习惯。
不带列表指定 -xalias,将假设程序不会违反 Fortran 别名使用规则。它等效于对所有别名使用关键字断言 no%。
不指定 -xalias 进行编译时,编译器缺省设置是:
-xalias=no%dummy,craypointer,no%actual,no%overindex,no%ftnpointer
如果程序使用 Cray 指针但符合 Fortran 别名使用规则(据此,即使在二义情况下指针引用也不可能导致别名使用),则用 -xalias 进行编译,结果可能会生成更好的优化代码。
传统代码可能包含普通计算 DO 循环的源代码重构,其目的是使旧式的向量化编译器生成用于特定体系结构的最佳代码。大多数情况下,这些重构不再需要了,而且可能会降低程序的可移植性。两种常见的重构分别是条状提取和循环展开。
有些体系结构中的固定长度矢量寄存器可让程序员手动将循环中的数组计算条状提取成各个段。
REAL TX(0:63) ... DO IOUTER = 1,NX,64 DO IINNER = 0,63 TX(IINNER) = AX(IOUTER+IINNER) * BX(IOUTER+IINNER)/2. QX(IOUTER+IINNER) = TX(IINNER)**2 END DO END DO |
条状提取对现代编译器已不再适合;可按如下方式编写循环来大大降低模糊程度:
DO IX = 1,N TX = AX(I)*BX(I)/2. QX(I) = TX**2 END DO |
在编译器可用之前手动展开循环是一项典型的源代码优化技巧,它会自动执行此重构。循环被编写为:
DO K = 1, N-5, 6 DO J = 1, N DO I = 1,N A(I,J) = A(I,J) + B(I,K ) * C(K ,J) * + B(I,K+1) * C(K+1,J) * + B(I,K+2) * C(K+2,J) * + B(I,K+3) * C(K+3,J) * + B(I,K+4) * C(K+4,J) * + B(I,K+5) * C(K+5,J) END DO END DO END DO DO KK = K,N DO J =1,N DO I =1,N A(I,J) = A(I,J) + B(I,KK) * C(KK,J) END DO END DO END DO |
应按其原始意图进行改写:
DO K = 1,N DO J = 1,N DO I = 1,N A(I,J) = A(I,J) + B(I,K) * C(K,J) END DO END DO END DO |