Sun Studio 12:Fortran 编程指南

第 6 章 浮点运算

本章分析浮点运算并提出避免和检测数值计算错误的策略。

有关 SPARC 和 x86 处理器浮点计算的详细说明,参见《数值计算指南》。

6.1 简介

SPARC 处理器上的 Fortran 95 浮点环境实现了“二进制浮点运算 IEEE 标准 754”指定的运算模型。该环境使您能够开发强健、高性能、可移植的数值应用程序。它还提供了用于分析研究数值程序任何不正常行为的工具。

在数值程序中,有许多潜在因素可引起计算错误:

找出数值计算中出错的原因非常困难。可以尽可能使用市面上销售的经过测试的库程序包来减少编码错误几率。算法的选择是另一个关键问题。使用合适的计算机运算同样也是一个关键问题。

本章不打算教授或解释数值错误分析。此处提供的资料旨在介绍 Fortran 95 实现的 IEEE 浮点模型。

6.2 IEEE 浮点运算

IEEE 运算是一种处理会导致如下问题的运算操作的相对较新的方法:无效操作数、被零除、上溢、下 溢或结果不精确。不同之处在于舍入、近零数字的处理以及接近机器最大值的数字的处理。

IEEE 标准支持用户处理异常、舍入和精度。因此,此标准支持区间运算和异常诊断。IEEE 标准 754 可以使诸如 expcos 之类的基本函数标准化,以便创建高精确度的运算,并结合数值和符号代数计算。

与其他任何种类的浮点运算相比,IEEE 运算向用户提供了对计算的更强大控制。此标准简化了编写复杂可移植数值程序的任务。很多有关浮点算法的问题都涉及数的基本运算。例如:

另一类问题与浮点异常及异常处理有关。如果进行下列运算会发生什么情况:

在较早的运算模型中,第一类问题可能不会得到预期的答案,而第二类问题中的异常情况可能都会得到相同的结果:程序立即中止或使用无用结果继续执行。

此标准可确保运算产生具有预期性质的、符合数学预期的结果。它还确保异常情况产生指定的结果,除非用户明确地指定其他选项。

例如,直观地引入了异常值 +Inf、-Inf 和 NaN:

big*big = +Inf 正无穷

big*(-big) = -Inf 负无穷

num/0.0 = +Inf 其中 num > 0.0

num/0.0 = -Inf 其中 num < 0.0

0.0/0.0 = NaN 非数字

并且,还标识出五种类型的浮点异常:

在《数值计算指南》中介绍了 IEEE 标准的实现。

6.2.1 –ftrap=mode 编译器选项

-ftrap=mode 选项可捕获浮点异常。如果 ieee_handler() 调用未建立信号处理程序,异常会用内存转储核心转储文件终止程序。有关该编译器选项的详细信息,请参见《Fortran 用户指南》。例如,为了能够捕获上溢、被零除和无效运算,可使用 -ftrap=common 进行编译。(这是 f95 的缺省选项。)


注 –

必须使用 -ftrap= 编译应用程序的主程序才能进行捕获。


6.2.2 浮点异常

f95 程序不会自动报告异常。要显示程序终止时产生的浮点异常列表,需要显式调用 ieee_retrospective(3M)。一般情况下,如果发生了无效、被零除或上溢异常中的任何一种,都会产生消息。不精确异常不会产生消息,因为它们在实际程序中发生得过于频繁。

6.2.2.1 回顾性摘要

ieee_retrospective函数对浮点状态寄存器进行查询,以找出产生了哪些异常,并打印一条有关标准错误的消息来通知您哪些是已产生但尚未清除的异常。此消息通常如下所示;格式可能会随各编译器版本而变化:


Note: IEEE floating-point exception flags raised:
    Division by Zero;
IEEE floating-point exception traps enabled:
    inexact;  underflow;  overflow;  invalid operation;
See the Numerical Computation Guide, ieee_flags(3M),
    ieee_handler(3M)

Fortran 95 程序需要显式调用 ieee_retrospective,并使用 -xlang=f77 进行编译,以便与 f77 兼容库进行链接。

-f77 兼容标志进行编译,将启用程序终止时自动调用 ieee_retrospective 惯例。

可以使用 ieee_flags() 关闭任意或所有这些消息,方法是在调用 ieee_retrospective 之前清除异常状态标记。

6.2.3 处理异常

根据 IEEE 标准进行异常处理是 SPARC 和 x86 处理器上的缺省异常处理方法。但是,检测浮点异常和生成浮点异常信号(SIGFPE)之间是有区别的。

按照 IEEE 标准,当浮点运算期间出现未捕获的异常时,会发生两件事情:

6.2.4 捕获浮点异常

f95 在处理浮点异常方面与早期的 f77 编译器有着明显的区别。

缺省情况下,f95 会自动捕获被零除、上溢和无效运算。而 f77 在缺省情况下会为浮点异常自动产生信号来中断正在运行的程序。其假设是:只要返回期望的值,大多数异常都无关紧要,对其进行捕获会降低性能。

可以使用 f95 命令行选项 -ftrap 来更改缺省设置。f95 的缺省选项是 -ftrap=common。要按早期的 f77 缺省设置进行运算,请使用 -ftrap=%none 编译主程序。

6.2.5 非标准运算

可以手动禁用标准 IEEE 运算中一个称为渐进下溢的特征。禁用后,程序将被视为在非标准运算下运行。

IEEE 运算标准规定了一种通过动态调整有效数的小数点来渐进处理下溢结果的方法。按 IEEE 浮点格式,小数点出现在有效数之前,并且有一个隐式前导位 1。当浮点计算结果会下溢时,渐进下溢允许将此隐式前导位清为 0,并将小数点移入有效数之中,否则,浮点计算结果便会产生下溢。对于 SPARC 处理器,该结果不是在硬件而是在软件中完成的。如果程序产生的下溢很多(也许这表示您的算法有问题),可能会导致性能损失。

可以通过以下方式禁用然后关闭渐进下溢: 使用 -fns 选项进行编译,或从程序内调用库例程 nonstandard_arithmetic()。调用 standard_arithmetic() 可重新开启渐进下溢。


注 –

为提高效率,必须用 -fns 编译应用程序的主程序。参见《Fortran 用户指南》。


对于传统应用程序,请注意:

6.3 IEEE 例程

以下接口可帮助用户使用 IEEE 运算,并在手册页中进行说明。这些接口多数都在数学库 libsunmath 和几个 .h 文件中。

6.3.1 标志和 ieee_flags()

ieee_flags 函数用于查询和清除异常状态标志。它是 Sun 编译器随带的 libsunmath 库的一部分,可执行下列任务:

ieee_flags 调用的一般形式为:


      
flags = ieee_flags( action, mode, in, out )

四个参数中的每一个都是字符串。输入为 actionmodein。输出为 outflagsieee_flags 是一个整数值函数。flags 中返回有用的信息,作为 1 位标志集合。有关完整信息,请参见 ieee_flags(3m) 手册页。

下表显示了所有可能的参数值。

表 6–1 ieee_flags ( action, mode, in, out ) 参数值

参数 

允许值 

action

get, set, clear, clearall

mode

direction, exception

in, out

nearest, tozero, negative, positive, extended, double single, inexact, division, underflow, overflow, invalid all, common

注意,这些是文字字符串,且输出参数 out 必须至少是 CHARACTER*9inout 的可能值的含义取决于与其一起使用的 action 和 mode。下表对此进行了概括:

表 6–2 ieee_flags in、 out 参数的含义

inout 的值

所指 

nearest, tozero, negative, positive

舍入方向 

extended, double, single

舍入精度 

inexact, division, underflow, overflow, invalid

异常 

all

全部五种异常 

common

常见异常:无效、除法、上溢 

例如,要确定引起了标志的具有最高优先级的异常,请将输入参数 in 作为空字符串传递:


      CHARACTER *9, out
      ieeer = ieee_flags( ’get’, ’exception’, ’’, out )
      PRINT *, out, ’ flag raised’

另外,要确定是否引起了 overflow 异常标志,请将输入参数 in 设置为 overflow。返回时,如果 out 等于 overflow,会出现 overflow 异常标志;否则不会出现该标志。


      ieeer = ieee_flags( ’get’, ’exception’, ’overflow’, out )
      IF ( out.eq. ’overflow’) PRINT *,’overflow flag raised’

示例:清除 invalid 异常:


      ieeer = ieee_flags( ’clear’, ’exception’, ’invalid’, out )

示例:清除所有异常:


      ieeer = ieee_flags( ’clear’, ’exception’, ’all’, out )

示例:将舍入方向设置为零:


      ieeer = ieee_flags( ’set’, ’direction’, ’tozero’, out )

示例:将舍入精度设置为 double


      ieeer = ieee_flags( ’set’, ’precision’, ’double’, out )

6.3.1.1 用 ieee_flags 关闭所有警告消息。

使用清除 action 调用 ieee_flags(如下例所示)可以重置任何未清除的异常。在程序退出之前进行该调用,可禁止系统在程序终止时产生浮点异常警告消息。

示例:用 ieee_flags() 清除所有产生的异常:


      i = ieee_flags(’clear’, ’exception’, ’all’, out )

6.3.1.2 用 ieee_flags 检测异常

以下示例演示如何确定早期计算引起的浮点异常。会将系统 include 文件 floatingpoint.h 中定义的位屏蔽应用于 ieee_flags 的返回值。

在以下示例(即 DetExcFlg.F)中,include 文件是使用 #include 预处理程序指令引入的,这就要求以 .F 后缀命名源文件。下溢是由最小的双精度数除以 2 引起的。

示例:使用 ieee_flags 检测异常,然后对其进行解码:


#include "floatingpoint.h"
         CHARACTER*16 out
         DOUBLE PRECISION d_max_subnormal, x
         INTEGER div, flgs, inv, inx, over, under

       x = d_max_subnormal() / 2.0                ! Cause underflow

       flgs=ieee_flags(’get’,’exception’,’’,out)  ! Which are raised?

       inx   = and(rshift(flgs, fp_inexact)  , 1)  ! Decode
       div   = and(rshift(flgs, fp_division) , 1)   ! the value
       under = and(rshift(flgs, fp_underflow), 1)    ! returned
       over  = and(rshift(flgs, fp_overflow) , 1)     ! by
       inv   = and(rshift(flgs, fp_invalid)  , 1)      ! ieee_flags

       PRINT *, "Highest priority exception is: ", out
       PRINT *, ’ invalid  divide  overflo underflo inexact’
       PRINT ’(5i8)’, inv, div, over, under, inx
       PRINT *, ’(1 = exception is raised; 0 = it is not)’
       i = ieee_flags(’clear’, ’exception’, ’all’, out)    ! Clear all
       END

示例:编译并运行上述示例 (DetExcFlg.F):


demo% f95 DetExcFlg.F
demo% a.out
 Highest priority exception is: underflow
  invalid  divide  overflo underflo inexact
       0       0       0       1       1
 (1 = exception is raised; 0 = it is not)
demo%

6.3.2 IEEE 极值函数

编译器提供了一个函数集,可以调用其中的函数来返回特殊的 IEEE 极值。这些值,如 infinityminimum normal,可以直接在应用程序中使用。

示例:基于硬件支持的最小数值的收敛测试如下所示:


      IF ( delta .LE. r_min_normal() ) RETURN

下表列出了可用的值:

表 6–3 返回 IEEE 值的函数

IEEE 值 

双精度 

单精度 

infinity

d_infinity()

r_infinity()

quiet NaN

d_quiet_nan()

r_quiet_nan()

signaling NaN

d_signaling_nan()

r_signaling_nan()

min normal

d_min_normal()

r_min_normal()

min subnormal

d_min_subnormal()

r_min_subnormal()

max subnormal

d_max_subnormal()

r_max_subnormal()

max normal

d_max_normal()

r_max_normal()

两个 NaN 值(quietsignaling)是无序的,不能用于比较,如 IF(X.ne.r_quiet_nan())THEN...。要确定某些值是否是 NaN,请使用函数 ir_isnan(r)id_isnan(d)

以下手册页列出了这些函数的 Fortran 名称:

另请参见:

6.3.3 异常处理程序和 ieee_handler()

关于 IEEE 异常,通常需要关注以下问题:

用户例程的异常捕获以系统产生浮点异常信号开始。signal: floating-point exception的 UNIX 标准名称是 SIGFPE。出现异常时,SPARC 平台上的缺省情况是产生 SIGFPE。要使系统产生 SIGFPE,必须先启用异常捕获,这通常通过对 ieee_handler() 的调用来完成。

6.3.3.1 建立异常处理程序函数

要将函数作为异常处理程序建立,请将函数名称与要监视的异常的名称和要采取的操作一起传递给 ieee_handler()。一旦建立了处理程序,无论何时出现特定的浮点异常和调用指定的函数,都会产生 SIGFPE 信号。

ieee_handler() 的调用形式如下表所示:

表 6–4 ieee_handler (action , exception , handler) 的参数

参数 

类型 

可能值 

action

character

getsetclear

exception

character

invaliddivisionoverflowunderflowinexact

handler

函数名 

用户处理函数的名称或 SIGFPE_DEFAULT、SIGFPE_IGNORESIGFPE_ABORT

返回值 

integer

0 =OK

f95 编译的、调用 ieee_handler() 的 Fortran 95 例程还应该声明:

#include ’floatingpoint.h’

特殊参数 SIGFPE_DEFAULTSIGFPE_IGNORESIGFPE_ABORT 定义在这些包含文件中,可用于更改与特定异常相应的程序行为:

SIGFPE_DEFAULT SIGFPE_IGNORE

出现指定异常时不采取任何操作。 

SIGFPE_ABORT

程序在异常时中止(可能会使用转储文件)。 

6.3.3.2 编写用户异常处理程序函数

异常处理程序采取的操作由您决定。但是,此例程必须是整型函数,且具有下面指定的三个参数:

handler_name( sig, sip, uap )

示例:异常处理程序函数:


      INTEGER FUNCTION hand( sig, sip, uap )
      INTEGER sig, location
      STRUCTURE /fault/
           INTEGER address
           INTEGER trapno
      END STRUCTURE
      STRUCTURE /siginfo/
           INTEGER si_signo
           INTEGER si_code
           INTEGER si_errno
           RECORD /fault/ fault
      END STRUCTURE
      RECORD /siginfo/ sip
      location = sip.fault.address
      ... actions you take ...
      END

此示例需要修改才能运行在 64 位 SPARC 体系结构上,方法是使用 INTEGER*8 替换每个 STRUCTURE 中的所有 INTEGER 声明。

如果由 ieee_handler() 启用的处理程序例程与此示例中一样,是用 Fortran 编写的,则此例程不能对其第一个参数 (sig) 进行任何引用。该第一个参数按值传递给此例程,并且只能作为 loc(sig) 进行引用。此值是信号编号。

通过处理程序检测异常

下列示例展示如何创建处理程序例程来检测浮点异常。

示例:检测异常并中止


demo% cat DetExcHan.f
      EXTERNAL myhandler
      REAL :: r = 14.2 , s = 0.0
      i = ieee_handler (’set’, ’division’, myhandler )
      t = r/s
      END

      INTEGER FUNCTION myhandler(sig,code,context)
      INTEGER sig, code, context(5)
      CALL abort()
      END
demo% f95 DetExcHan.f
demo% a.out
Abort
demo%

SIGFPE 可在浮点异常出现的任何时间产生。检测到 SIGFPE 时,控制将传递给 myhandler 函数,该函数会立即中止。用 -g 编译,并使用 dbx 查找异常位置。

通过处理程序定位异常

示例:定位异常(打印地址)并中止:


demo% cat LocExcHan.F
#include "floatingpoint.h"
      EXTERNAL Exhandler
      INTEGER Exhandler, i, ieee_handler
      REAL:: r = 14.2 ,  s = 0.0 , t
C  Detect division by zero
      i = ieee_handler( ’set’, ’division’, Exhandler )
      t = r/s
      END

      INTEGER FUNCTION Exhandler( sig, sip, uap)
      INTEGER sig
      STRUCTURE /fault/
                  INTEGER address
      END STRUCTURE
      STRUCTURE /siginfo/
                  INTEGER si_signo
                  INTEGER si_code
                  INTEGER si_errno
                  RECORD /fault/ fault
      END STRUCTURE
      RECORD /siginfo/ sip
      WRITE (*,10)  sip.si_signo, sip.si_code, sip.fault.address
10        FORMAT(’Signal ’,i4,’ code ’,i4,’ at hex address ’, Z8 )
      Exhandler=1
      CALL abort()
      END
demo%  f95 -g LocExcHan.F
demo%  a.out
Signal   8 code    3 at hex address    11230
Abort
demo%

在 64 位 SPARC 环境中,请用 INTEGER*8 替换每个 STRUCTURE 中的 INTEGER 声明,用 i8 替换 i4 格式。(注意,该例接受 VAX Fortran STRUCTURE 语句,依靠的是 f95 编译器的扩展。)

大多数情况下,知道异常的实际地址并无太大用处,但对于 dbx 除外:


demo% dbx a.out
(dbx) stopi at 0x11230     Set breakpoint at address
(2) stopi at &MAIN+0x68
(dbx) run               Run program
Running: a.out
(process id 18803)
stopped in MAIN at 0x11230
MAIN+0x68:      fdivs   %f3, %f2, %f2
(dbx) where           Shows the line number of the exception
=>[1] MAIN(), line 7 in "LocExcHan.F"
(dbx) list 7          Displays the source code line
    7         t = r/s
(dbx) cont            Continue after breakpoint, enter handler routine
Signal    8 code    3 at hex address    11230
abort: called
signal ABRT (Abort) in _kill at 0xef6e18a4
_kill+0x8:      bgeu    _kill+0x30
Current function is exhandler
   24         CALL abort()
(dbx) quit
demo%

当然,还有更容易的方法来确定引起错误的源码行。但是,本例确实足以展示异常处理的基本内容。

6.4 调试 IEEE 异常

定位异常出现的位置需要启用异常捕获。这可以通过使用 -ftrap=common 选项(用 f95 编译器编译时的缺省选项)进行编译,或通过使用 ieee_handler() 建立异常处理程序例程来完成。启用了异常捕获,便可从 dbx 中运行程序,使用 dbx catch FPE 命令来查看出错位置。

使用 -ftrap=common 编译的优点是:无需修改源代码即可捕获异常。但是,通过调用 ieee_handler(),您可以更有选择性地查看异常。

示例:dbx 的编译及使用:


demo% f95 -g myprogram.f
demo% dbx a.out
Reading symbolic information for a.out
...
(dbx) catch FPE
(dbx) run
Running: a.out
(process id 19739)
signal FPE (floating point divide by zero) in MAIN at line 212 in file "myprogram.f"
  212               Z = X/Y
(dbx)  print Y
y = 0.0
(dbx)

如果发现程序因上溢和其他异常而终止,可调用 ieee_handler() 明确定位第一处上溢,以便只捕获上溢。这至少需要修改主程序的源代码,如下例所示。

示例:在其他异常出现时定位上溢:


demo% cat myprog.F
#include “floatingpoint.h”
        program myprogram
...
      ier = ieee_handler(”set’,’overflow’,SIGFPE_ABORT)
...
demo% f95 -g myprog.F
demo% dbx a.out
Reading symbolic information for a.out
...
(dbx) catch FPE
(dbx) run
Running: a.out
(process id 19793)
signal FPE (floating point overflow) in MAIN at line 55 in file "myprog.F"
   55               w = rmax * 200.                     ! Cause of the overflow
(dbx) cont                                   ! Continue execution to completion
execution completed, exit code is 0
(dbx)

为了具有选择性,此示例引入了 #include,它需要以 .F 后缀重命名源文件,并调用 ieee_handler()。您可更深入一层,创建出现上溢异常时要调用的自己的处理程序函数,执行一些应用程序特定的分析,并在中止前打印中间结果或调试结果。

6.5 更深层次的数值风险

本部分解决一些运算操作无意间可能会产生无效、被零除、上溢、下溢或不精确异常的实际问题。

例如,在 IEEE 标准之前,如果在计算机中将两个非常小的数相乘,结果可能为零。多数大型机和小型机的行为亦是如此。使用 IEEE 运算,渐进下溢会扩大动态计算范围。

例如,假设某一 32 位处理器以 1.0E-38 作为机器中可表示的最小值 epsilon。将两个小数相乘:


      a = 1.0E-30
      b = 1.0E-15
      x = a * b

较早的运算会得到 0.0,但如果使用 IEEE 运算和相同的字长,却会得到 1.40130E-45。此时便出现了下溢,告诉您答案比机器自然表示的值小。该结果是通过从尾数中“窃取”一些位并将其移交给指数来完成的。得到的结果(即一个非规范化数)从某种意义上说精确度比较低,但从另外一种意义上说精确度又比较高。其深层含意已超出了本次讨论的范围。如果有兴趣,可以参考《Computer》1980 年 1 月,第 13 卷,第 1 期,尤其是 J. Coonen 的文章 "Underflow and the Denormalized Numbers"。

大多数科学程序都有对舍入很敏感的代码段,通常是在方程求解或矩阵因子分解中。若不采用渐进下溢,程序员就得自己实现检测接近不准确阈值的方法。否则,他们就必须放弃对实现强大、稳定算法的追求。

有关这些主题的详细信息,请参见《数值计算指南》。

6.5.1 避免简单下溢

有些应用程序实际上执行了许多非常接近零的计算。这在计算残数或微分修正的算法中很常见。为获得在数值上安全的最大性能,需要采用扩展精度运算来执行关键计算。如果应用程序是单精度应用程序,可采用双精度执行关键计算。

示例:采用单精度的简单点积计算:


      sum = 0
      DO i = 1, n
         sum = sum + a(i) * b(i)
      END DO

如果 a(i)b(i) 非常小,会出现很多下溢。通过强制计算采用双精度,计算点积时会具有更高的准确性,并且可避免出现下溢情况:


      DOUBLE PRECISION sum
      DO i = 1, n
         sum = sum + dble(a(i)) * dble(b(i))
      END DO
      result = sum

通过增加对库例程 nonstandard_arithmetic() 的调用,或通过使用 -fns 选项编译应用程序的主程序,可以强制 SPARC 处理器在涉及到下溢时(存储零)像较早的系统一样进行处理。

6.5.2 以错误答案继续

您可能会对在答案明显错误的情况下为什么能继续进行计算感到奇怪。IEEE 运算允许您区分可以忽略的错误答案的种类,如 NaNInf。然后,可以根据此种区分来作决定。

不妨考虑一个电路模拟的例子。在 50 行的特定计算中,(出于参数原因)唯一关注的变量是电压。进一步假设其值只可能是 +5v、0、-5v。

仔细安排计算的每一部分以强制每个子结果在正确范围内,这是很可能实现的:

此外,由于 Inf 不是允许值,所以需要特殊的逻辑来确保不会与较大的数相乘。

利用 IEEE 运算,此逻辑可以简化许多。计算可以用显而易见的方式编写,并且只需强制最终结果为正确的值-因为 Inf 可以出现并且可以很容易地测出。

此外,还可检测到 0/0 的特殊情况并按照您的意愿进行处理。结果更易读取且执行起来更快,因为无需进行不必要的比较。

6.5.3 过度下溢

如果将两个非常小的数字相乘,结果将会出现下溢。

如果预知乘法(或减法)中的操作数可能会很小并且很可能会下溢,可采用双精度进行计算,而后再将结果转换成单精度。

例如,类似下面的点积循环:


  real sum, a(maxn), b(maxn)
  ...
  do i =1, n
      sum = sum + a(i)*b(i)
  enddo

其中,已知 a(*)b(*) 具有小元素,为保持数值准确度,应采用双精度来运行:


  real a(maxn), b(maxn)
  double sum
  ...
  do i =1, n
      sum = sum + a(i)*dble(b(i))
  enddo

鉴于以软件方式解决了原始循环造成的过度下溢,这样做还有可能提高性能。但是,对此并无绝对而快速的法则;只能通过对计算代码进行高强度实验来确定最有利的解决方案。

6.6 区间运算

注意:目前,区间运算仅适用于 SPARC 平台。

Fortran 95 编译器 f95 支持将区间作为内在数据类型。区间是封闭的紧集:[a, b] ={z | a≤ zb} (由一对数字定义,ab )。区间可以用于:

通过将区间作为一种内在数据类型引入 Fortran 95,开发人员立即可以使用 Fortran 95 的所有适用语法和语义。除了 INTERVAL 数据类型外,f95 还包括 Fortran 95 的下列区间扩展:

f95 命令行选项 -xinterval 可启用编译器的区间运算功能。参见《Fortran 用户指南》。

有关 Fortran 95 中区间运算的详细信息,请参见《Fortran 95 区间运算编程参考》。