C++ 平方差的数值精度

C++ 平方差的数值精度,c++,c,numerical-analysis,C++,C,Numerical Analysis,在我的代码中,我经常计算以下内容(为了简单起见,这里是C代码): 对于本例,忽略平方根的参数可能由于不精确而为负。我通过额外的fdimf呼叫解决了这个问题。然而,我想知道以下是否更准确: float sin_theta = sqrtf((1.0f + cos_theta) * (1.0f - cos_theta)); // Option 2 cos_theta介于-1和+1之间,因此对于每个选择,都会出现我减去类似数字的情况,从而降低精度,对吗什么是最精确的?为什么?使用浮点的最精确方法可能是

在我的代码中,我经常计算以下内容(为了简单起见,这里是C代码):

对于本例,忽略平方根的参数可能由于不精确而为负。我通过额外的
fdimf
呼叫解决了这个问题。然而,我想知道以下是否更准确:

float sin_theta = sqrtf((1.0f + cos_theta) * (1.0f - cos_theta)); // Option 2

cos_theta
介于
-1
+1
之间,因此对于每个选择,都会出现我减去类似数字的情况,从而降低精度,对吗什么是最精确的?为什么?

使用浮点的最精确方法可能是使用单个x87指令计算正弦波和余弦波

但是,如果您需要手动进行计算,最好将具有类似量级的参数分组。这意味着第二个选项更精确,尤其是当
cos_theta
接近0时,精度最为重要

作为文章 注:

表达式x2-y2是另一个表现出灾难性后果的公式 取消。将其评估为(x-y)(x+y)更为准确

编辑:比这更复杂。尽管上述情况通常正确,(x-y)(x+y)在x和y具有非常不同的量级时,精确度稍低,如该陈述的脚注所述:

在这种情况下,(x-y)(x+y)有三个舍入误差,但x2-y2只有两个舍入误差,因为计算x2和y2中较小者时产生的舍入误差不会影响最终减法

换句话说,取x-y、x+y和乘积(x-y)(x+y)都会引入舍入误差(舍入误差的3个步骤)。x2、y2和减法x2-y2也都会引入舍入误差,但是通过将相对较小的数字(x和y中较小的一个)平方得到的舍入误差可以忽略不计,因此实际上只有两个舍入误差步骤,使得平方差更加精确


因此,选项1实际上将更加精确。dev.brutus的Java测试证实了这一点。

[为major think-o编辑]在我看来,选项2会更好,因为对于像
0.000001这样的数字,例如选项1将正弦值返回为1,而选项将返回一个略小于1的数字。

我编写了一个小测试。它以双精度计算期望值。然后,它会计算您的选项的错误。第一种选择更好:

Algorithm: FloatTest$1
option 1 error = 3.802792362162126
option 2 error = 4.333273185303996
Algorithm: FloatTest$2
option 1 error = 3.802792362167937
option 2 error = 4.333273185305868
Java代码:

import org.junit.Test;
公开课浮动测试{
@试验
公开无效测试(){
testImpl(新的ExpectedAlgorithm(){
公共双te(双cos_theta){
返回Math.sqrt(1.0f-cos_theta*cos_theta);
}
});
testImpl(新的ExpectedAlgorithm(){
公共双te(双cos_theta){
返回Math.sqrt((1.0f+cos_θ)*(1.0f-cos_θ));
}
});
}
公共无效测试MPL(预期算法ea){
双delta1=0;
双delta2=0;

对于(double cos_theta=-1;cos_theta来说,当θ很小时,你总是会遇到问题,因为θ=0附近的余弦是平的。如果θ在-0.0001和0.0001之间,那么浮点中的cos(theta)正好是一,所以你的sin_theta正好是零

为了回答您的问题,当cos_θ接近1时(对应于一个小θ),您的第二次计算显然更精确。下面的程序显示了这一点,该程序列出了不同cos_θ值的两次计算的绝对和相对误差。通过与使用GNU MP库以200位精度计算的值进行比较来计算误差,然后将其转换为浮点

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

int main() 
{
  int i;
  printf("cos_theta       abs (1)    rel (1)       abs (2)    rel (2)\n\n");
  for (i = -14; i < 0; ++i) {
    float x = 1 - pow(10, i/2.0);
    float approx1 = sqrt(1 - x * x);
    float approx2 = sqrt((1 - x) * (1 + x));

    /* Use GNU MultiPrecision Library to get 'exact' answer */
    mpf_t tmp1, tmp2;
    mpf_init2(tmp1, 200);  /* use 200 bits precision */
    mpf_init2(tmp2, 200);
    mpf_set_d(tmp1, x);
    mpf_mul(tmp2, tmp1, tmp1);  /* tmp2 = x * x */
    mpf_neg(tmp1, tmp2);        /* tmp1 = -x * x */
    mpf_add_ui(tmp2, tmp1, 1);  /* tmp2 = 1 - x * x */
    mpf_sqrt(tmp1, tmp2);       /* tmp1 = sqrt(1 - x * x) */
    float exact = mpf_get_d(tmp1);

    printf("%.8f     %.3e  %.3e     %.3e  %.3e\n", x,
           fabs(approx1 - exact), fabs((approx1 - exact) / exact),
           fabs(approx2 - exact), fabs((approx2 - exact) / exact));
    /* printf("%.10f  %.8f  %.8f  %.8f\n", x, exact, approx1, approx2); */
  }
  return 0;
}

当cos_theta不接近1时,两种方法的精度非常接近,并对误差进行舍入。

我的选择没有区别,因为(1-x)保留了精度,而不影响进位。然后对于(1+x)同样的道理。那么影响进位精度的唯一因素就是乘法。因此,在这两种情况下,都有一次乘法,因此它们都可能产生相同的进位错误。

对某些表达式的数字精度进行推理的正确方法是:

  • 测量相对于(最后一位的单位)中正确值的结果差异,该值由W.H.Kahan于1960年引入。您可以找到C、Python和Mathematica实现,并了解有关该主题的更多信息
  • 根据两个或多个表达式产生的最坏情况,而不是其他答案中的平均绝对误差或其他任意度量来区分它们。这就是数值近似多项式的构造方法(),标准库方法的实现分析方法(例如Intel)等
  • 考虑到这一点,版本_1:sqrt(1-x*x)和版本_2:sqrt((1-x)*(1+x))产生了显著不同的结果。如下图所示,版本_1在x接近1时表现出灾难性性能,错误>1_000_000 ulps,而另一方面版本_2的错误表现良好

    这就是为什么我总是建议使用版本2,即利用平方差公式

    生成square_diff_error.csv文件的Python 3.6代码:

    from fractions import Fraction
    from math import exp, fabs, sqrt
    from random import random
    from struct import pack, unpack
    
    
    def ulp(x):
        """
        Computing ULP of input double precision number x exploiting
        lexicographic ordering property of positive IEEE-754 numbers.
    
        The implementation correctly handles the special cases:
          - ulp(NaN) = NaN
          - ulp(-Inf) = Inf
          - ulp(Inf) = Inf
    
        Author: Hrvoje Abraham
        Date: 11.12.2015
        Revisions: 15.08.2017
                   26.11.2017
        MIT License https://opensource.org/licenses/MIT
    
        :param x: (float) float ULP will be calculated for
        :returns: (float) the input float number ULP value
        """
    
        # setting sign bit to 0, e.g. -0.0 becomes 0.0
        t = abs(x)
    
        # converting IEEE-754 64-bit format bit content to unsigned integer
        ll = unpack('Q', pack('d', t))[0]
    
        # computing first smaller integer, bigger in a case of ll=0 (t=0.0)
        near_ll = abs(ll - 1)
    
        # converting back to float, its value will be float nearest to t
        near_t = unpack('d', pack('Q', near_ll))[0]
    
        # abs takes care of case t=0.0
        return abs(t - near_t)
    
    
    with open('e:/square_diff_error.csv', 'w') as f:
        for _ in range(100_000):
            # nonlinear distribution of x in [0, 1] to produce more cases close to 1
            k = 10
            x = (exp(k) - exp(k * random())) / (exp(k) - 1)
    
            fx = Fraction(x)
            correct = sqrt(float(Fraction(1) - fx * fx))
    
            version1 = sqrt(1.0 - x * x)
            version2 = sqrt((1.0 - x) * (1.0 + x))
    
            err1 = fabs(version1 - correct) / ulp(correct)
            err2 = fabs(version2 - correct) / ulp(correct)
    
            f.write(f'{x},{err1},{err2}\n')
    
    Mathematic生成最终绘图的代码:

    data = Import["e:/square_diff_error.csv"];
    
    err1 = {1 - #[[1]], #[[2]]} & /@ data;
    err2 = {1 - #[[1]], #[[3]]} & /@ data;
    
    ListLogLogPlot[{err1, err2}, PlotRange -> All, Axes -> False, Frame -> True,
        FrameLabel -> {"1-x", "error [ULPs]"}, LabelStyle -> {FontSize -> 20}]
    

    double
    通常比
    float
    精度更高@PeteBecker:And
    long double
    精度更高,但是
    float
    对我来说速度更快。
    1.0f-cos_theta*cos_theta
    最精确的计算可能是
    fmaf(cos_theta,-cos_theta,1.0f)
    。但是,如果您非常关心速度,不想在中间计算中使用
    double
    ,那么您不应该在不提供单个har的处理器上使用
    fmaf
    cos_theta       abs (1)    rel (1)       abs (2)    rel (2)
    
    0.99999988     2.910e-11  5.960e-08     0.000e+00  0.000e+00
    0.99999970     5.821e-11  7.539e-08     0.000e+00  0.000e+00
    0.99999899     3.492e-10  2.453e-07     1.164e-10  8.178e-08
    0.99999684     2.095e-09  8.337e-07     0.000e+00  0.000e+00
    0.99998999     1.118e-08  2.497e-06     0.000e+00  0.000e+00
    0.99996835     6.240e-08  7.843e-06     9.313e-10  1.171e-07
    0.99989998     3.530e-07  2.496e-05     0.000e+00  0.000e+00
    0.99968380     3.818e-07  1.519e-05     0.000e+00  0.000e+00
    0.99900001     1.490e-07  3.333e-06     0.000e+00  0.000e+00
    0.99683774     8.941e-08  1.125e-06     7.451e-09  9.376e-08
    0.99000001     5.960e-08  4.225e-07     0.000e+00  0.000e+00
    0.96837723     1.490e-08  5.973e-08     0.000e+00  0.000e+00
    0.89999998     2.980e-08  6.837e-08     0.000e+00  0.000e+00
    0.68377221     5.960e-08  8.168e-08     5.960e-08  8.168e-08
    
    from fractions import Fraction
    from math import exp, fabs, sqrt
    from random import random
    from struct import pack, unpack
    
    
    def ulp(x):
        """
        Computing ULP of input double precision number x exploiting
        lexicographic ordering property of positive IEEE-754 numbers.
    
        The implementation correctly handles the special cases:
          - ulp(NaN) = NaN
          - ulp(-Inf) = Inf
          - ulp(Inf) = Inf
    
        Author: Hrvoje Abraham
        Date: 11.12.2015
        Revisions: 15.08.2017
                   26.11.2017
        MIT License https://opensource.org/licenses/MIT
    
        :param x: (float) float ULP will be calculated for
        :returns: (float) the input float number ULP value
        """
    
        # setting sign bit to 0, e.g. -0.0 becomes 0.0
        t = abs(x)
    
        # converting IEEE-754 64-bit format bit content to unsigned integer
        ll = unpack('Q', pack('d', t))[0]
    
        # computing first smaller integer, bigger in a case of ll=0 (t=0.0)
        near_ll = abs(ll - 1)
    
        # converting back to float, its value will be float nearest to t
        near_t = unpack('d', pack('Q', near_ll))[0]
    
        # abs takes care of case t=0.0
        return abs(t - near_t)
    
    
    with open('e:/square_diff_error.csv', 'w') as f:
        for _ in range(100_000):
            # nonlinear distribution of x in [0, 1] to produce more cases close to 1
            k = 10
            x = (exp(k) - exp(k * random())) / (exp(k) - 1)
    
            fx = Fraction(x)
            correct = sqrt(float(Fraction(1) - fx * fx))
    
            version1 = sqrt(1.0 - x * x)
            version2 = sqrt((1.0 - x) * (1.0 + x))
    
            err1 = fabs(version1 - correct) / ulp(correct)
            err2 = fabs(version2 - correct) / ulp(correct)
    
            f.write(f'{x},{err1},{err2}\n')
    
    data = Import["e:/square_diff_error.csv"];
    
    err1 = {1 - #[[1]], #[[2]]} & /@ data;
    err2 = {1 - #[[1]], #[[3]]} & /@ data;
    
    ListLogLogPlot[{err1, err2}, PlotRange -> All, Axes -> False, Frame -> True,
        FrameLabel -> {"1-x", "error [ULPs]"}, LabelStyle -> {FontSize -> 20}]