Floating point 为什么(1-x)(1+;x)比(1-x^2)更可取?

Floating point 为什么(1-x)(1+;x)比(1-x^2)更可取?,floating-point,Floating Point,我正在研究一个运行时库实现的arcin,它是通过计算: ArcTan(X, Sqrt(1 - X*X)) 但是,计算1-X*X的代码实际计算(1-X)*(1+X)。是否有充分的理由选择后者?我怀疑后者会减少接近零的X的舍入误差,但我无法解释为什么会这样。我编写了下面的程序,以获得单精度的一些经验结果 #include <float.h> #include <math.h> #include <stdio.h> long double d1, d2, rel

我正在研究一个运行时库实现的
arcin
,它是通过计算:

ArcTan(X, Sqrt(1 - X*X))

但是,计算
1-X*X
的代码实际计算
(1-X)*(1+X)
。是否有充分的理由选择后者?我怀疑后者会减少接近零的
X
的舍入误差,但我无法解释为什么会这样。

我编写了下面的程序,以获得单精度的一些经验结果

#include <float.h>
#include <math.h>
#include <stdio.h>

long double d1, d2, rel1, rel2;
float i1, i2;
int main() {
  float f;
  for (f = nextafterf(0, 2); f <= 1; f = nextafterf(f, 2))
    {
      long double o = 1.0L - ((long double)f * f);
      float r1 = (1 - f) * (1 + f);
      float r2 = 1 - f * f;
      long double c1 = fabsl(o - r1);
      long double c2 = fabsl(o - r2);
      if (c1 > d1) d1 = c1;
      if (c2 > d2) d2 = c2;
      if (c1 / o > rel1) rel1 = c1 / o, i1 = f;
      if (c2 / o > rel2) rel2 = c2 / o, i2 = f;
    }

  printf("(1-x)(1+x) abs:%Le  relative:%Le\n", d1, rel1);
  printf("1-x*x      abs:%Le  relative:%Le\n\n", d2, rel2);

  printf("input1: %a 1-x:%a 1+x:%a (1-x)(1+x):%a o:%a\n", i1, 1-i1, 1+i1, (1-i1)*(1+i1), (double)(1 - ((long double)i1 * i1)));
  printf("input2: %a x*x:%a 1-x*x:%a o:%a\n", i2, i2*i2, 1 - i2*i2, (double)(1 - ((long double)i2 * i2)));
}
根据这些结果,
1-x*x
具有更好的绝对精度,
(1-x)*(1+x)
具有更好的相对精度。浮点是关于相对精度的(整个系统的设计允许相对准确地表示大小值),因此后一种形式是首选

编辑:计算最终错误更有意义,如Eric的回答所示。选择表达式中的子表达式,例如
ArcTan(X,Sqrt(1-X*X))
并不是因为其总体准确性更好,而是因为它在最重要的地方是准确的。将以下行添加到循环体:

  long double a = atan2l(f, sqrtl(o));
  float a1 = atan2f(f, sqrtf(r1));
  float a2 = atan2f(f, sqrtf(r2));
  long double e1 = fabsl(a - a1);
  long double e2 = fabsl(a - a2);
  if (e1 / a > ae1) ae1 = e1 / a, i1 = f;
  if (e2 / a > ae2) ae2 = e2 / a, i2 = f;

使用
atan2l(f,sqrtf(r1))
可能很有意义,因为我没有与您的系统完全相同的功能
ArcTan
。无论如何,有了这些注意事项,对于完整表达式,[-1…1]区间上的最大相对误差对于(1-x)(1+x)版本为1.4e-07,对于1-x2版本为5.5e-7。

ArcTan(x,Sqrt(1-x*x))对x的导数是
1/Sqrt(1-x*x)
。当| X |变为1时,它变为无穷大。因此,当X接近1或-1时,计算中的任何错误都会对结果产生巨大影响。因此,在这些情况下,使评估误差最小化是至关重要的

当X接近1时,
1-X
的计算没有错误(在IEEE 754或任何良好的浮点系统中,因为结果的规模使得其最低有效位至少与1或X中的最低有效位一样低,因此精确的数学结果没有超出可用有效位的位)。由于<代码> 1-x是精确的,考虑了<代码> 1 +x>代码>中的误差的影响,考虑了<代码> ARCTAN(x,Sqrt((1-x)*(1 +x+e))< /> >关于E,其中E是在<代码> 1 +x操作中引入的误差。当x接近1,E小时,导数是-1/10。(用Maple取导数,用1代替x得到
-1/(sqrt(4+2e)*(5+2e)
。然后用0代替e得到-1/10。)因此
1+x中的错误并不严重

因此,将表达式求值为
ArcTan(X,Sqrt((1-X)*(1+X))
是一种很好的求值方法

对于接近-1的X,情况是对称的。(
1+X
没有错误,
1-X
不是关键。)

<> P,相反,如果在<代码> x*x</代码>中考虑,<代码> ARCTAN(x,Sqrt(1-x*x+e))< /> >关于E是,当x接近1时,大约为1 /(2qRT(e)(1 +e)),所以当E很小时它是大的,因此当x接近1时,在评价<代码> x*x< /代码>中的一个小错误将导致结果中的大误差。
当计算函数f(x)时,请询问Pascal Cuoq指出的问题,我们通常对最小化最终结果中的相对误差感兴趣。正如我所指出的,由于浮点舍入,计算过程中出现的误差通常是中间结果中的相对误差。我在上面可以忽略这一点,因为我在考虑X接近1时的函数,所以将考虑中的值(1+X和X*X)命名为中间值,最终值的震级接近1,因此将这些值除以这些震级不会显著改变任何东西

然而,为了完整性,我更仔细地检查了情况。在Maple中,我分别编写了
g:=arctan(x,sqrt((1-x*x*(1+e0))*(1+e1))*(1+e2))
,从而考虑了
x*x
1-x*x
,以及
sqrt
,计算中的相对误差e0、e1和e2,并编写了
h:=arctan(x,sqrt((1-x)*(1+x)*(1+e0))*(1+e2))
作为替代方案。请注意,在这种情况下,e0将
1-x
1+x
中的三个错误以及它们的乘法组合在一起;完整的错误项可以是
(1+ea)*(1+eb)*(1+ec)
,但这实际上是
1+e0

然后,我研究了这些函数关于(一次一个)e0、e1和e2除以abs(f(x))的导数,其中
f
是理想函数,
arctan(x,sqrt(1-x*x))
。例如,在Maple中,我研究了
diff(g,e0)/abs(f(x))
。我没有对这些进行全面的分析评估;我检查了一些接近0和接近1的x值,以及e0、e1和e2值在其一个限值-2-54处的值

对于接近0的x,所有值的大小都在1或更小。也就是说,计算中的任何相对误差都会导致结果中类似的相对误差,或更小

对于接近1的x,e1和e2导数的值很小,约为10-8或更小。但是,对于这两种方法,e0导数的值相差很大。对于
1-x*x
方法,该值约为2•107(使用x=1-2-53)。对于
(1-x)*(1+x)
方法,该值约为5•10-9


总之,这两种方法在接近x=0时没有太大区别,但
(1-x)*(1+x)
方法在接近x=1时明显更好。

是的,您理解正确。这是关于舍入精度的问题。这与您的具体情况不完全相关,但对于对数计算,这种转换可能非常有用:
log(1-x*x)如果x接近零(
1E-12
),而
log(1-x)+l
  long double a = atan2l(f, sqrtl(o));
  float a1 = atan2f(f, sqrtf(r1));
  float a2 = atan2f(f, sqrtf(r2));
  long double e1 = fabsl(a - a1);
  long double e2 = fabsl(a - a2);
  if (e1 / a > ae1) ae1 = e1 / a, i1 = f;
  if (e2 / a > ae2) ae2 = e2 / a, i2 = f;