本章讨论从其他平台向 Fortran 95 移植“旧式”Fortran 程序时可能产生的一些问题。
Fortran 95 扩展和 Fortran 77 兼容功能在《Fortran 用户指南》中介绍。
Fortran 在最初开发 Fortran 时,回车控制就不受所用设备的功能限制。出于类似的历史原因,源自 UNIX 的操作系统不具备 Fortran 回车控制,但您可以用 Fortran 95 编译器以两种方式对其进行模拟。
在用 lpr 命令打印文件之前,使用 asa 过滤器将 Fortran 回车控制惯例转换成 UNIX 回车控制格式(参见 asa (1) 手册页)。
FORTRAN 77 编译器 f77 允许 OPEN(N, FORM=’PRINT’) 启用单倍行距或双倍行距、换页以及剥离第一列。这还可通过将 FORM=’PRINT’ 与 f95 -f77 兼容标志一起使用来编译程序的方式而获得。此编译器允许在使用 -f77 编译时,重新打开单元 6,以将形式参数更改为 PRINT。例如:
OPEN( 6, FORM=’PRINT’) |
可以使用 lp(1) 打印以这种方式打开的文件。
早期的 Fortran 系统不使用命名文件,但提供了一种命令行机制,使实际文件名可以与内部单元编号等同。可以用多种方式模拟该功能,其中包括标准 UNIX 重定向。
示例:将 stdin 重定向至 redir.data(使用 csh(1)):
demo% cat redir.data 数据文件 9 9.9 demo% cat redir.f 源文件 read(*,*) i, z 程序读取标准输入 print *, i, z stop end demo% f95 -o redir redir.f 编译步骤 demo% redir < redir.data 运行重定向读取数据文件 9 9.90000 demo% |
如果应用程序代码最初是为 64 位(或 60 位)大型机开发的,如 CRAY 或 CDC,则在移植到 UltraSPARC 平台时可能要使用以下选项来编译这些代码,例如:
-fast -m64 -xtypemap=real:64,double:64,integer:64
这些选项自动将所有缺省 REAL 变量和常量提升至 REAL*8,将 COMPLEX 提升至 COMPLEX*16。只有未声明的变量或声明为简单 REAL 或 COMPLEX 的变量才会得到提升;显式声明的变量(例如,REAL*4)不会被提升。所有单精度 REAL 常量也会被提升为 REAL*8。(针对目标平台相应地设置 -xarch 和 -xchip。)若要将缺省 DOUBLE PRECISION 数据也提升为 REAL*16,请将 -xtypemap 示例中的 double:64 更改为 double:128。
有关详细信息,参见《Fortran 用户指南》或 f95(1) 手册页。
《Fortran 用户指南》和《数值计算指南》详细讨论了 Fortran 中数据对象的硬件表示。跨系统和硬件平台的数据表示之间的差别通常会产生最严重的可移植问题。
应注意下列问题:
Sun 遵守浮点运算“IEEE 标准 754”。因此,REAL*8 中的头四个字节与 REAL*4 中的头四个字节不同。
实数、整数和逻辑值的缺省大小在 Fortran 95 标准中进行了说明,不过这些缺省大小可以通过 -xtypemap 选项进行更改。
可以自由混合字符变量并使其等效于其他类型的变量,但需注意潜在的对齐问题。
f95 IEEE 浮点运算会在上溢或被零除时引起异常,并发送 SIGFPE 信号或在缺省情况下捕获异常(对于 f95,缺省选项为 -ftrap=common)。在某些情况下,它只能传送 IEEE 不定式,否则,就将以信号方式通知异常。第 6 章,浮点运算一章对此进行了说明。
可以确定有限、正规化的极值。参见 libm_single(3F) 和 libm_double(3F)。不定式可以使用格式化、列表控制的 I/O 语句进行读写。
许多“旧式”Fortran 应用程序会将霍尔瑞斯 ASCII 数据存储到数值数据对象中。在 1977 Fortran 标准(以及 Fortran 95)中,为此目的提供了 CHARACTER 数据类型,并建议使用。您仍可利用早先的 Fortran 霍尔瑞斯 (nH) 功能对变量进行初始化,但这不是标准做法。下表指明了适合某种数据类型的最大字符数。(在本表中,粗体数据类型指示应通过 -xtypemap 命令行标志提升缺省类型。)
表 7–1 数据类型的最大字符数
|
最大标准 ASCII 字符数 |
|
|
|
---|---|---|---|---|
数据类型 |
缺省 |
INTEGER:64 |
REAL:64 |
DOUBLE:128 |
BYTE |
1 |
1 |
1 |
1 |
COMPLEX |
8 |
8 |
16 |
16 |
COMPLEX*16 |
16 |
16 |
16 |
16 |
COMPLEX*32 |
32 |
32 |
32 |
32 |
DOUBLE COMPLEX |
16 |
16 |
32 |
32 |
DOUBLE PRECISION |
8 |
8 |
16 |
16 |
INTEGER |
4 |
8 |
4 |
8 |
INTEGER*2 |
2 |
2 |
2 |
2 |
INTEGER*4 |
4 |
4 |
4 |
4 |
INTEGER*8 |
8 |
8 |
8 |
8 |
LOGICAL |
4 |
8 |
4 |
8 |
LOGICAL*1 |
1 |
1 |
1 |
1 |
LOGICAL*2 |
2 |
2 |
2 |
2 |
LOGICAL*4 |
4 |
4 |
4 |
4 |
LOGICAL*8 |
8 |
8 |
8 |
8 |
REAL |
4 |
4 |
8 |
8 |
REAL*4 |
4 |
4 |
4 |
4 |
REAL*8 |
8 |
8 |
8 |
8 |
REAL*16 |
16 |
16 |
16 |
16 |
示例:用霍尔瑞斯初始化变量:
demo% cat FourA8.f double complex x(2) data x /16Habcdefghijklmnop, 16Hqrstuvwxyz012345/ write( 6, ’(4A8, "!")’ ) x end demo% f95 -o FourA8 FourA8.f demo% FourA8 abcdefghijklmnopqrstuvwxyz012345! demo% |
如果需要,可以用霍尔瑞斯初始化具有兼容类型的数据项,然后将其传递给其他例程。
如果将霍尔瑞斯常量作为参数传递,或者将其用在表达式或比较中,它们将被解释为字符型表达式。使用编译器选项 -xhasc=no,可以让编译器在子程序调用时将参数中的霍尔瑞斯常量视作无类型数据。在移植较早的 Fortran 程序时可能需要这样做。
一般情况下,通过消除所有非标准编码,可以更简单地将一个应用程序从一个系统和编译器移植到另一个系统和编译器。在一个系统中大获成功的优化或解决方法,在其他系统中可能只会给编译器造成模糊和混乱。特别是,针对某一特定体系结构所做的优化手动调节,在别处可能会造成性能下降。对此将在后面关于性能和调节的章节中予以讨论。但是,就一般性移植而言,下列问题值得考虑。
有些系统会自动将局部变量和 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 |
返回日期时间或经过的 CPU 时间的库函数会因系统的不同而不同。
Fortran 库中支持的时间函数列在下表中:
表 7–3 Fortran 时间函数
名称 |
功能 |
手册页 |
---|---|---|
time |
返回自 1970 年 1 月 1 日以来经过的秒数 |
time(3F) |
date |
以字符串形式返回日期 |
date(3F) |
fdate |
以字符串形式返回当前时间和日期 |
fdate(3F) |
idate |
在整型数组中返回当前的年、月、日 |
idate(3F) |
itime |
在整型数组中返回当前的时、分、秒 |
itime(3F) |
ctime |
将 time 函数返回的时间转换成字符串 |
ctime(3F) |
ltime |
将 time 函数返回的时间转换成本地时间 |
ltime(3F) |
gmtime |
将 time 函数返回的时间转换成格林威治时间 |
gmtime(3F) |
etime |
单处理器:返回程序执行经过的用户及系统时间 多处理器:返回挂钟时间 |
etime(3F) |
dtime |
返回自上次调用 dtime 以来经过的用户及系统时间 |
dtime(3F) |
date_and_time |
以字符和数字形式返回日期及时间 |
date_and_time(3F) |
有关详细信息,参见《Fortran 库参考手册 》或这些函数各自的手册页。下面给出了一个使用这些时间函数的简单示例 (TestTim.f):
subroutine startclock common / myclock / mytime integer mytime, time mytime = time() return end function wallclock() integer wallclock common / myclock / mytime integer mytime, time, newtime newtime = time() wallclock = newtime– mytime mytime = newtime return end integer wallclock, elapsed character*24 greeting real dtime, timediff, timearray(2) c print a heading call fdate( greeting ) print*, " Hello, Time Now Is: ", greeting print*, "See how long ’sleep 4’ takes, in seconds" call startclock call system( ’sleep 4’ ) elapsed = wallclock() print*, "Elapsed time for sleep 4 was: ", elapsed," seconds" c now test the cpu time for some trivial computing timediff = dtime( timearray ) q = 0.01 do 30 i = 1, 100000 q = atan( q ) 30 continue timediff = dtime( timearray ) print*, "atan(q) 100000 times took: ", timediff ," seconds" end |
运行该程序会产生以下结果:
demo% TimeTest Hello, Time Now Is: Thu Feb 8 15:33:36 2001 See how long ’sleep 4’ takes, in seconds Elapsed time for sleep 4 was: 4 seconds atan(q) 100000 times took: 0.01 seconds demo% |
下表中所列的这些例程提供了与 VMS Fortran 系统例程 idate 和 time 的兼容性。要使用这些例程,必须在 f95 命令行中加入 -lV77 选项,此时还会得到这些 VMS 版本,而非标准 f95 版本。
表 7–4 汇总:非标准 VMS Fortran 系统例程
名称 |
定义 |
调用序列 |
参数类型 |
---|---|---|---|
idate |
日期为年、月、日形式 |
call idate( d, m, y ) |
integer |
time |
当前时间为 hhmmss 形式 |
call time( t ) |
character*8 |
date(3F) 例程和 VMS 版本的 idate(3F) 存在 2000 年安全问题,因为它们返回包含两位数值的年份。通过减去这些例程返回的日期来计算持续时间的程序,在 1999 年 12 月 31 日之后,其计算结果将是错误的。应改用 Fortran 95 例程 date_and_time(3F)。有关详细信息,参见《Fortran 库参考手册》。
此处给出了一些建议,适用于移植到 Fortran 95 的程序没有按预期运行的情形。
注意大小和工程单位。非常接近于零的数字表面上似乎有区别,但区别并不显著,特别是当该数是两个大数之差时。例如,1.9999999e-30 非常接近于 -9.9992112e-33,即使它们在符号上有区别。
VAX 数学运算不如 IEEE 数学运算精确,甚至不同的 IEEE 处理器都可能会有区别。特别是当数学运算涉及很多三角函数时,更是如此。这些函数比人们想象的要复杂得多,而且标准只定义了基本运算函数。即使是在 IEEE 机器之间,也可能存在微妙的差别。回顾有关浮点问题的第 6 章,浮点运算一章。
尝试用 call nonstandard_arithmetic() 运行。这样做还可大幅提高性能,并使 Sun 工作站操作起来更象是 VAX 系统。如果您有权访问 VAX 或某一其他系统,请照此行事。很多数值应用程序在每一种浮点实现上产生的结果会稍微有些不同,这一点相当常见。
检查 NaN、+Inf 及其他可能的错误迹象。有关如何捕获各种异常的说明,请参见第 6 章,浮点运算一章或手册页 ieee_handler(3m)。在大多数机器上,这些异常只是中止运行。
相差 6 x 1029 的两个数仍可以具有相同的浮点形式。在以下示例中,不同数字具有相同的表示形式:
real*4 x,y x=99999990e+29 y=99999996e+29 write (*,10) x, x 10 format(’99,999,990 x 10^29 = ’, e14.8, ’ = ’, z8) write(*,20) y, y 20 format(’99,999,996 x 10^29 = ’, e14.8, ’ = ’, z8) end |
输出为:
99,999,990 x 10^29 = 0.99999993E+37 = 7CF0BDC1 99,999,996 x 10^29 = 0.99999993E+37 = 7CF0BDC1 |
在本例中,差别达 6 x 1029。IEEE 单精度运算是造成此种无法区分的巨大差距的原因,对于任一十进制到二进制的转换,只能保证六位十进制数字。您有可能能够正确转换七位或八位数字,但这取决于具体的数字。
如果程序失败而不发出警告,并且失败之间运行的时间长度不同,则:
用最小优化 (–O1) 进行编译。如果此时程序工作正常,请以更高的优化级别只编译挑选出的例程。
优化程序必须对程序作出假设,应理解这一点。非标准编码或构造均能造成问题。几乎没有任何优化程序能以所有优化级别处理所有程序。(请参见7.6.2 别名使用和 -xalias 选项)