调用pow()时四舍五入结果的差异

Differences in rounded result when calling pow()

本文关键字:结果 四舍五入 pow 调用      更新时间:2023-10-16

好的,我知道有很多关于pow函数和将结果转换为int的问题,但是我找不到这个有点具体问题的答案。

好的,这是C代码:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main()
{
    int i = 5;
    int j = 2;
    double d1 = pow(i,j);
    double d2 = pow(5,2);
    int i1 = (int)d1;
    int i2 = (int)d2;
    int i3 = (int)pow(i,j);
    int i4 = (int)pow(5,2);
    printf("%d %d %d %d",i1,i2,i3,i4);
    return 0;
}

输出:"25 25 24 25"。注意,只有在第三种情况下,pow的参数不是字面量,我们才会得到错误的结果,这可能是由舍入错误引起的。没有显式强制转换也会发生同样的事情。有人能解释一下这四种情况下发生了什么吗?

我在Windows 7中使用CodeBlocks,以及附带的MinGW gcc编译器

pow运算的结果是2500000加上或减去一些舍入误差。如果舍入误差为正或零,则转换为整数后的结果为25。如果舍入误差为负,则结果为24。

内部最可能发生的情况是,在一种情况下,直接使用更高精度的80位FPU值,而在另一种情况下,结果从FPU写入内存(作为64位double),然后再读回(将其转换为稍微不同的80位值)。这可以使最终结果产生微小的差异,这就是将25.0000000001更改为24.999999997所需要的全部

另一种可能性是编译器识别传递给pow的常量并自己进行计算,将结果替换为对pow的调用。你的编译器可能会使用一个内部任意精度的数学库,也可能只是使用一个不同的。

这是由两个问题共同引起的:

  • 您正在使用的pow实施质量不高。在许多情况下,浮点运算必须是近似的,但是好的实现会注意确保像pow(5, 2)这样的简单情况返回精确的结果。您正在使用的pow返回的结果小于25,其值大于0,但小于或等于2 -49 。例如,它可能返回25-2 -50
  • 您使用的C实现有时使用64位浮点格式,有时使用80位浮点格式。只要数字保持在80位格式,它就保留pow返回的完整值。如果将此值转换为整数,则会产生24,因为该值小于25,转换为整数会被截断;它不圆。当数字转换为64位格式时,它是四舍五入的。在浮点格式之间进行转换,使结果四舍五入到最接近的可表示值25。之后,转换为整数产生25。
在某种意义上,编译器可以在"方便"的时候切换格式。例如,使用80位格式的寄存器数量有限。当它们已满时,编译器可能会将一些值转换为64位格式并将它们存储在内存中。编译器也可以在编译时而不是运行时重新排列表达式或执行部分表达式,这些会影响执行的算术和使用的格式。

当C实现混合浮点格式时是很麻烦的,因为用户通常无法预测或控制格式之间的转换何时发生。这导致结果不容易重现,并干扰推导或控制软件的数值特性。C实现可以设计成始终使用单一格式并避免其中一些问题,但是您的C实现显然不是这样设计的。

在这里添加其他答案:在处理浮点值时,通常要非常小心。

我强烈推荐阅读这篇文章(尽管它很长):http://hal.archives-ouvertes.fr/docs/00/28/14/29/PDF/floating-point-article.pdf

跳过第3节的实际例子,但不要忽略前面的章节!

我很确定这可以通过"中间舍入"来解释,并且pow不是简单地循环j乘以i,而是使用exp(log(i)*j)作为浮点计算来计算。中间舍入可以很好地将24.99999999999996转换为25000000000 -即使任意存储和重新加载值也可能导致这种行为的差异,因此根据代码的生成方式,它可能会对确切的结果产生影响。

当然,在某些情况下,编译器甚至可能"知道"pow实际实现了什么,并将计算结果替换为常数。