【问题标题】:Underflow error in floating point arithmetic in CC中浮点运算中的下溢错误
【发布时间】:2021-11-06 12:16:45
【问题描述】:

我是 C 新手,我的任务是创建一个函数

f(x) = sqrt[(x^2)+1]-1

它可以处理非常大的数字和非常小的数字。我正在在线界面上提交我的脚本以检查我的答案。

对于非常大的数字,我将表达式简化为:

f(x) = x-1

只需使用最高功率。这是正确的答案。

同样的逻辑不适用于较小的数字。对于小数(大约 1e-7),它们会很快被截断为零,甚至在平方之前。我怀疑这与 C 中的浮点精度有关。在我的教科书中,它说浮点类型的最小可能值为 1.17549e-38,精度为 6 位。所以虽然 1e-7 比 1.17e-38 大很多,但它的精度更高,因此四舍五入为零。这是我的猜测,如果我错了,请纠正我。

作为一种解决方案,我认为当 x

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

double feval(double x) {
    /* Insert your code here */
    if (x > 1e299) 
    {;
        return x-1;
    }
    if (x < 1e-6)
    {
        long double g;
        g = x;
        printf("x = %Lf\n", g);
        long double a;
        a = pow(x,2);
        printf("x squared = %Lf\n", a);
        return sqrt(g*g+1.)- 1.;
    }
    else
    { 
        printf("x = %f\n", x);
        printf("Used third \n");
        return sqrt(pow(x,2)+1.)-1;
    }
}

int main(void)
{
    double x;
    printf("Input: ");
    scanf("%lf", &x);
    double b;
    b = feval(x);
    printf("%f\n", b);
    return 0;
}

【问题讨论】:

  • 请注意,pow 返回一个double。事后将其转换为long double 不会改变这一点。如果结果不适合double,它将溢出。如果你想要 long double 结果,那么你需要使用 powl 来代替。

标签: c floating-point underflow


【解决方案1】:

在这些情况下通常有用的一个技巧是基于身份

(a+1)*(a-1) = a*a-1

在这种情况下

sqrt(x*x+1)-1 = (sqrt(x*x+1)-1)*(sqrt(x*x+1)+1) 
                 /(sqrt(x*x+1)+1)
= (x*x+1-1) / (sqrt(x*x+1)+1)
= x*x/(sqrt(x*x+1)+1)

最后一个公式可以作为实现。对于 vwry small x sqrt(x*x+1)+1 将接近 2(对于足够小的 x 它将是 2)但我们不会放松评估它的精度。

【讨论】:

  • 问题似乎是 x 在进入计算之前就被截断为零。如果我将 1e-7 输入到 scanf 中,那么当我打印它时它会显示为 0.0000。我的 scanf 有问题吗?
  • @lcleary 你的打印效果如何?如果您只打印 5 个小数位,那么 0.0 是正确的打印值。尝试使用 %e 打印它
【解决方案2】:

以朴素的方式实现这里有两个问题:计算x * x时中间计算中的溢出或下溢,以及最终减1期间的减法取消。第二个问题是准确性问题。

ISO C 有一个标准数学函数hypot (x, y),它可以准确地执行sqrt (x * x + y * y) 的计算,同时避免中间计算中的下溢和上溢。解决减法消除问题的一种常见方法是对计算进行代数转换,以便将其转换为乘法和/或除法。

结合这两个修复导致float 参数的以下实现。根据我的测试,它在所有可能的输入中的错误小于 3 ulps

/* Compute sqrt(x*x+1)-1 accurately and without spurious overflow or underflow */
float func (float x)
{
    return (x / (1.0f + hypotf (x, 1.0f))) * x;
}

【讨论】:

  • 问题似乎是 x 在进入计算之前就被截断为零。如果我将 1e-7 输入到 scanf 中,那么当我打印它时它会显示为 0.0000。我的 scanf 有问题吗?
  • @lcleary 如果将 1e-7 (0.0000001) 打印为只有四位小数的普通旧十进制数,它将打印为零。尝试不同的printf() 格式说明符以获得一些流畅性。这是一个完整的入门程序:#include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; int main (void) { float x; scanf ("%f", &amp;x); printf ("x=%23.16e\n", x); return EXIT_SUCCESS; }。 1e-7 并不完全可以作为 float 来表示,所以这个程序会打印出类似 x=1.0000000116860974e-007 的内容。
【解决方案3】:

问题不在于跑到最小值,而在于精度。

正如您自己所说,您机器上的float 的精度约为 7 位数。所以让我们取 x = 1e-7,所以 x^2 = 1e-14。这仍然在float 的范围内,没有问题。但现在加 1。确切的答案是1.00000000000001。但如果我们只有 7 位精度,则将四舍五入为 1.0000000,即正好 1。所以你最终计算出的 sqrt(1.0)-1 正好是 0。

一种方法是使用sqrtx=1sqrt(x) ~ 1+0.5*(x-1) 周围的线性近似。这将导致近似f(x) ~ 0.5*x^2

【讨论】:

  • 问题似乎是 x 在进入计算之前就被截断为零。如果我将 1e-7 输入到 scanf 中,那么当我打印它时它会显示为 0.0000。我的 scanf 有问题吗?
  • @lcleary:您得到了正确的值,但打印时没有打印足够的小数位。 printf("%f") 默认为 6 位数。试试printf("%.10f")
【解决方案4】:

对于较小的输入,执行 1+x^2 时会出现截断错误。如果x=1e-7fx*x 将很高兴地适合 32 位浮点数(由于1e-7 没有精确的浮点表示,但x*x 会很多小于 1 表示浮点精度不足以表示 1+x*x

对 sqrt(1+x^2) 进行泰勒展开会更合适,最低阶为

sqrt(1+x^2) = 1 + 0.5*x^2 + O(x^4)

然后,你可以把你的结果写成

sqrt(1+x^2)-1 = 0.5*x^2 + O(x^4),

避免将非常小的数字添加到 1 的情况。

附带说明,您不应将pow 用于整数幂。对于 x^2,你应该只做x*x。任意整数幂的效率要高一些。例如,GNU 科学库具有高效计算任意整数幂的功能。

【讨论】:

  • **编辑:我明白,我把它倒过来了。这是针对靠近a的点x,而不是靠近x的点。我的理解是,泰勒级数近似的形式是 f(x) ~ f(a) + f'(a)(x-a) + ... (在某个点 a 接近 x)。因此,如果我们将 a 设为 1e-7,我们仍然需要计算 f(a),这会给我们带来同样的问题,不是吗?即使取你这里的东西,你也有 1 + 0.5x^2,其中 x^2 将是一个非常小的数字。那么如何避免我们将 1 添加到一个非常小的数字的情况呢?
  • 关于“作为一个旁注,你不应该使用pow 来表示整数幂”:这对于整数幂可能是合理的建议,但是对于更大的整数幂, pow 可能比复合乘法更准确。即使对于大小适中的整数,忠实四舍五入的pow 也可能比乘法更准确。
  • @lcleary 我们通过在代数中减去 1 来避免将小数加到 1,因此最终结果仅为 0.5x^2。您只需从您的函数中返回 0.5*x*x 即可获得小的 x
  • 谢谢,这是一个聪明的把戏。尽管问题似乎是 x 在进入计算之前就被截断为零。如果我将 1e-7 输入到 scanf 中,那么当我打印它时它会显示为 0.0000。我的 scanf 有问题吗?
猜你喜欢
  • 2023-03-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多