这些双精度值如何精确到20位小数



当精度是一个问题时,我正在测试一些非常简单的等价错误,并希望以扩展的双精度执行操作(这样我就知道大约19位的答案),然后以双精度执行相同的操作(第16位会有舍入错误),但不知怎么的,我的双精度算术保持了19位数的精度。

当我用扩展双精度执行运算,然后将数字硬编码到另一个Fortran例程中时,我会得到预期的错误,但当我在这里将扩展双精度变量分配给双精度变量时,会发生什么奇怪的事情吗?

program code_gen
    implicit none 
    integer, parameter :: Edp = selected_real_kind(17)
    integer, parameter :: dp = selected_real_kind(8)
    real(kind=Edp) :: alpha10, x10, y10, z10 
    real(kind=dp) :: alpha8, x8, y8, z8
    real(kind = dp) :: pi_dp = 3.1415926535897932384626433832795028841971693993751058209749445
    integer :: iter
    integer :: niters = 10
    print*, 'tiny(x10) = ', tiny(x10)
    print*, 'tiny(x8)  = ', tiny(x8)
    print*, 'epsilon(x10) = ', epsilon(x10)
    print*, 'epsilon(x8)  = ', epsilon(x8)
    do iter = 1,niters
        x10 = rand()
        y10 = rand()
        z10 = rand()
        alpha10 = x10*(y10+z10)
        x8 = x10 
        x8 = x8 - pi_dp
        x8 = x8 + pi_dp
        y8 = y10 
        y8 = y8 - pi_dp
        y8 = y8 + pi_dp
        z8 = z10 
        z8 = z8 - pi_dp
        z8 = z8 + pi_dp
        alpha8 = alpha10
        write(*, '(a, es30.20)') 'alpha8 .... ', x8*(y8+z8)
        write(*, '(a, es30.20)') 'alpha10 ... ', alpha10
        if( alpha8 .gt. x8*(y8+z8) ) then
            write(*, '(a)') 'ERROR(.gt.)'
        elseif( alpha8 .lt. x8*(y8+z8) ) then
            write(*, '(a)') 'ERROR(.lt.)'
        endif
    enddo
end program code_gen

其中CCD_ 1是在这里找到的gfortran函数。

如果我们只谈论一种精度类型(例如,double),那么我们可以将机器ε表示为E16,其近似为2.22E-16。如果我们对两个实数x+y进行简单相加,则得到的机器表示数是(x+y)*(1+d1),其中abs(d1) < E16。同样,如果我们把这个数字乘以z,得到的值实际上是(z*((x+y)*(1+d1))*(1+d2)),它几乎是(z*(x+y)*(1+d1+d2)),其中rand()0。如果我们现在转向扩展的双精度,那么唯一改变的是E16变为E20,并且其值约为1.08E-19

我希望以扩展的双精度进行分析,这样我就可以比较应该相等的两个数字,但也表明,有时舍入误差会导致比较失败。通过分配x8=x10,我希望创建一个扩展的双精度值x10的双精度"版本",其中只有x8的前~16位数字符合x10的值,但在打印出值时,它显示所有20位数字都是相同的,并且预期的双精度舍入误差并没有像我预期的那样发生。

还应该注意的是,在尝试之前,我写了一个程序,它实际上写了另一个程序,其中xyz的值被"硬编码"到小数点后20位。在该版本的程序中,.gt..lt.的比较按预期失败,但我无法通过将扩展的双精度值作为双精度变量来复制相同的失败。

为了进一步"干扰"双精度值并添加舍入误差,我从我的双精度变量中添加了pi,然后进行了减法运算,这应该会给剩余的变量留下一些双精度舍入误差,但我仍然没有在最终结果中看到这一点。

当您链接的gfortran文档声明时,rand的函数结果是默认的实值(单精度)。这样一个值可以用其他每一种真实类型精确地表示。

也就是说,x10=rand()将单个精度值分配给扩展精度变量x10。它确实如此。现在存储在x10中的这个相同值被分配给双精度变量x8,但它仍然可以精确地表示为双精度。

single-as-double中有足够的精度,因此使用double和扩展类型的计算返回相同的值。[参见本答案末尾的注释。]

如果您希望看到精度损失的实际影响,请从使用扩展或双精度值开始。例如,与其使用rand(返回单个精度值),不如使用固有的random_number

call random_number(x10)

(它具有作为标准Fortran的优点)。与函数不同,函数在(几乎)所有情况下都会返回值类型,而不管值的最终用途如何,此子例程将为您提供与参数相对应的精度。你会(希望)从你的"硬编码"实验中看到很多。

或者,正如agentp所评论的,从双精度值开始可能更直观

call random_number(x8); x10=x8   ! x8 and x10 have the precision of double precision
call random_number(y8); y10=y8
call random_number(z8); z10=z8

并从这个起点进行计算:然后这些额外的比特将开始显示。

总之,当您执行x8=x10时,您得到的是x8的前几个比特与x10的比特相对应,但这些比特中的许多比特以及x10中的后续比特都为零。

当涉及到pi_dp扰动时,您再次将单精度(这次是文字常量)值分配给双精度变量。仅仅拥有所有这些数字并不能使它成为默认的真实文字。您可以使用_Edp后缀指定不同类型的文字,如其他答案中所述。

最后,还必须担心编译器在优化方面做了什么。


我的论点是,从单精度值开始,所执行的计算可以精确地以双倍精度和扩展精度表示(具有相同的值)。对于其他计算,或者从设置了更多位的起点开始,或者表示(例如,在一些系统或其他编译器中,selected_real_kind(17)类型的数字类型可能具有完全不同的特性,例如不同的基数),而不必是这种情况。

虽然这在很大程度上是基于猜测和希望,但它解释了观察结果。幸运的是,有一些方法可以检验这个想法。当我们讨论IEEE算术时,我们可以考虑不精确标志。如果在计算过程中没有升起这个标志,我们会很高兴的。

对于gfortran,有一个编译选项-ffpe=inexact,它将发出不精确的标志信号。在gfortran 5.0中,支持可移植/标准方式使用的内部模块ieee_exceptions

你可以考虑这个标志进行进一步的实验:如果它被提升,那么你可以看到两种精度之间的差异。

最新更新