Sun Studio 12:Fortran 编程指南

第 7 章 移植

本章讨论从其他平台向 Fortran 95 移植“旧式”Fortran 程序时可能产生的一些问题。

Fortran 95 扩展和 Fortran 77 兼容功能在《Fortran 用户指南》中介绍。

7.1 回车控制

Fortran 在最初开发 Fortran 时,回车控制就不受所用设备的功能限制。出于类似的历史原因,源自 UNIX 的操作系统不具备 Fortran 回车控制,但您可以用 Fortran 95 编译器以两种方式对其进行模拟。


      OPEN( 6, FORM=’PRINT’)

可以使用 lp(1) 打印以这种方式打开的文件。

7.2 使用文件

早期的 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%

7.3 从科学大型机移植

如果应用程序代码最初是为 64 位(或 60 位)大型机开发的,如 CRAY 或 CDC,则在移植到 UltraSPARC 平台时可能要使用以下选项来编译这些代码,例如:

-fast -m64 -xtypemap=real:64,double:64,integer:64

这些选项自动将所有缺省 REAL 变量和常量提升至 REAL*8,将 COMPLEX 提升至 COMPLEX*16。只有未声明的变量或声明为简单 REALCOMPLEX 的变量才会得到提升;显式声明的变量(例如,REAL*4)不会被提升。所有单精度 REAL 常量也会被提升为 REAL*8。(针对目标平台相应地设置 -xarch-xchip。)若要将缺省 DOUBLE PRECISION 数据也提升为 REAL*16,请将 -xtypemap 示例中的 double:64 更改为 double:128

有关详细信息,参见《Fortran 用户指南》或 f95(1) 手册页。

7.4 数据表示

Fortran 用户指南和《数值计算指南》详细讨论了 Fortran 中数据对象的硬件表示。跨系统和硬件平台的数据表示之间的差别通常会产生最严重的可移植问题。

应注意下列问题:

7.5 霍尔瑞斯数据

许多“旧式”Fortran 应用程序会将霍尔瑞斯 ASCII 数据存储到数值数据对象中。在 1977 Fortran 标准(以及 Fortran 95)中,为此目的提供了 CHARACTER 数据类型,并建议使用。您仍可利用早先的 Fortran 霍尔瑞斯 (nH) 功能对变量进行初始化,但这不是标准做法。下表指明了适合某种数据类型的最大字符数。(在本表中,粗体数据类型指示应通过 -xtypemap 命令行标志提升缺省类型。)

表 7–1 数据类型的最大字符数

 

最大标准 ASCII 字符数 

 

 

 

数据类型 

缺省 

INTEGER:64

REAL:64

DOUBLE:128

BYTE

COMPLEX

16 

16 

COMPLEX*16

16 

16 

16 

16 

COMPLEX*32

32 

32 

32 

32 

DOUBLE COMPLEX

16 

16 

32 

32 

DOUBLE PRECISION

16 

16 

INTEGER

INTEGER*2

INTEGER*4

INTEGER*8

LOGICAL

LOGICAL*1

LOGICAL*2

LOGICAL*4

LOGICAL*8

REAL

REAL*4

REAL*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 程序时可能需要这样做。

7.6 非标准编码措施

一般情况下,通过消除所有非标准编码,可以更简单地将一个应用程序从一个系统和编译器移植到另一个系统和编译器。在一个系统中大获成功的优化或解决方法,在其他系统中可能只会给编译器造成模糊和混乱。特别是,针对某一特定体系结构所做的优化手动调节,在别处可能会造成性能下降。对此将在后面关于性能和调节的章节中予以讨论。但是,就一般性移植而言,下列问题值得考虑。

7.6.1 未初始化的变量

有些系统会自动将局部变量和 COMMON 变量初始化为零或者“非数字”(NaN) 值。但是,没有任何标准做法,而且程序不应对任何变量的初始值进行假设。要确保最大程度的可移植性,程序应初始化所有变量。

7.6.2 别名使用和 -xalias 选项

当同一个存储地址被多个名称引用时,就会出现别名使用情况。这种情况通常发生在指针上,或者是在子程序的实际参数相互重叠或与子程序内的 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 关键字及其含义

-xalias= keyword

别名使用情况 

dummy

伪子程序参数既可以互为别名,也可以作为全局变量的别名。 

no%dummy

遵循 Fortran 标准,伪参数在实际调用中既不互为别名也不作为全局变量的别名。(这是缺省情况。) 

craypointer

程序使用可指向任何地址的 Cray 指针。(这是缺省情况。) 

no%craypointer

Cray 指针总是指向明确的内存区,或者不被使用。 

ftnpointer

任何 Fortran 95 指针都可以指向任一目标变量,而不管其为何类型、种类或等级。 

no%ftnpointer

Fortran 95 指针遵守标准规则。(这是缺省情况。) 

overindex

在数组引用中违反下标边界可造成四种索引越界情况,其中的任何一种或多种都可以在程序中出现: 

  • 对 COMMON 块中数组元素的引用可以引用 COMMON 块或等价组中的任何元素。

  • 作为子程序的实际参数传递 COMMON 块或等价组的某一元素,将允许对该 COMMON 块或等价组中的任何元素进行访问。

  • 会将序列派生类型的变量当作是 COMMON 块一样来看待,并且此种变量的元素可以作为该变量其他元素的别名。

  • 可以违反各个数组下标边界,即使数组引用仍在该数组内。

    overindex 不适用于数组语法、WHEREFORALL 语句。如果索引越界出现在这些构造中,应将它们改写为 DO 循环。

no%overindex

不违反数组边界。数组引用不引用其他变量。(这是缺省情况。) 

actual

编译器将子程序的实际参数视为全局变量。向子程序传递参数有可能通过 Cray 指针导致别名使用。 

no%actual

向子程序传递参数不会进一步造成别名使用。(这是缺省情况。) 

这里列举了别名使用情况的一些典型示例。在较高优化级别(-O3 及其以上级别)上,如果您的程序中不存在如下所示的别名使用弊病,并且您是用 -xalias=no%keyword 进行编译的,f95 编译器可以生成更好的代码。

在某些情况下,您需要使用 -xalias=keyword 进行编译,以确保代码生成将会产生正确的结果。

7.6.2.1 通过伪参数和全局变量别名使用

下例需要使用 -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)
编译器必须假设伪变量和公用变量可以重叠。

7.6.2.2 随 Cray 指针带来的别名使用

本例仅在使用 -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 指针不指向重叠内存区。

7.6.2.3 随 Fortran 95 指针带来的别名使用

-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 中是非法的。如果编译器能够检测到此种情况,它会发出警告。

7.6.2.4 由索引越界造成的别名使用

-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 不适用于数组语法表达式、数组段、WHEREFORALL 语句。

为确保生成代码的正确性,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 进行编译后,就正常了。但应完全避免这种编程习惯。

7.6.2.5 由实际参数造成的别名使用

编译器超前查看局部变量是如何使用的,然后假设变量不会随子程序调用而变化。在下例中,子程序中使用的指针使编译器优化策略失败,并且结果无法预料。要使此例正确工作,需用 -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 标准的编程习惯。

7.6.2.6 -xalias 缺省设置

不带列表指定 -xalias,将假设程序不会违反 Fortran 别名使用规则。它等效于对所有别名使用关键字断言 no%

不指定 -xalias 进行编译时,编译器缺省设置是:

-xalias=no%dummy,craypointer,no%actual,no%overindex,no%ftnpointer

如果程序使用 Cray 指针但符合 Fortran 别名使用规则(据此,即使在二义情况下指针引用也不可能导致别名使用),则用 -xalias 进行编译,结果可能会生成更好的优化代码。

7.6.3 模糊优化

传统代码可能包含普通计算 DO 循环的源代码重构,其目的是使旧式的向量化编译器生成用于特定体系结构的最佳代码。大多数情况下,这些重构不再需要了,而且可能会降低程序的可移植性。两种常见的重构分别是条状提取和循环展开。

7.6.3.1 条状提取

有些体系结构中的固定长度矢量寄存器可让程序员手动将循环中的数组计算条状提取成各个段。


  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

7.6.3.2 循环展开

在编译器可用之前手动展开循环是一项典型的源代码优化技巧,它会自动执行此重构。循环被编写为:


  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

7.7 时间和日期函数

返回日期时间或经过的 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 系统例程 idatetime 的兼容性。要使用这些例程,必须在 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 库参考手册》。


7.8 疑难解答

此处给出了一些建议,适用于移植到 Fortran 95 的程序没有按预期运行的情形。

7.8.1 结果贴近,但不够贴近

尝试以下操作:


      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 单精度运算是造成此种无法区分的巨大差距的原因,对于任一十进制到二进制的转换,只能保证六位十进制数字。您有可能能够正确转换七位或八位数字,但这取决于具体的数字。

7.8.2 程序失败而不警告

如果程序失败而不发出警告,并且失败之间运行的时间长度不同,则: