具有较高尾数的fp如何表示较小的数字?

问题描述 投票:6回答:4

我爱FP;每次我想到它,我明白了解它:)

This是一个我不明白的例子。我总结8次相同的数字(0.1),我打印结果,总和和“原始”:

std::cout.precision(100);

int numIteration = 8;
double step = 0.1;
double sum = 0.0;

for(int i = 0; i < numIteration; i++) {
    sum += step;
}

std::cout << "orig stored as " << numIteration / 10.0 << std::endl;
std::cout << " sum stored as " << sum << std::endl;

0.1存储为0.1000000000000000055511151231257827021181583404541015625,我希望在8和之后,它将存储大于或等于0.8,存储为0.8000000000000000444089209850062616169452667236328125

但结果震惊了我。实际上经过8次总和,结果是0.79999999999999993338661852249060757458209991455078125,它更小。

另外,如果我检查两者的二进制输出,我可以看到总和比“原始”更“高”:

0.8 stored as binary 0 01111111110 1001100110011001100110011001100110011001100110011001 // smaller
sum stored as binary 0 01111111110 1001100110011001100110011001100110011001100110011010 // higher

但是0.79999999999999993338661852249060757458209991455078125 <0.8000000000000000444089209850062616169452667236328125

你能照我吗?

编辑:对不起,我复制/粘贴二进制文件时出错。他们是对的。

c++ floating-point
4个回答
7
投票

随着IEEE floating-point rounding在每次算术运算后发生。四舍五入可能会上升或下降。如果在每次迭代时打印sum的值,您应该看到:

sum is 0.1000000000000000055511151231257827021181583404541015625
sum is 0.200000000000000011102230246251565404236316680908203125
sum is 0.3000000000000000444089209850062616169452667236328125
sum is 0.40000000000000002220446049250313080847263336181640625
sum is 0.5
sum is 0.59999999999999997779553950749686919152736663818359375
sum is 0.6999999999999999555910790149937383830547332763671875
sum is 0.79999999999999993338661852249060757458209991455078125

你假设四舍五入只能上升。但是,由于“Round to nearest,ties to even”是IEEE 754中的默认舍入模式,因此在每次迭代时都会选择最接近的二进制可表示值,因此结果不必大于0.8

另一方面

std::cout << 0.1 * 8.0 << std::endl;

会产生预期的

0.8000000000000000444089209850062616169452667236328125

更新:如注释中提到的@Evg,可以使用std::fesetround更改浮点舍入方向。


2
投票

你的二进制表示是错误的。正确的是:

sum = 0.79999999999999993 ... = 
0b0011111111101001100110011001100110011001100110011001100110011001

numIteration / 10.0 = 0.80000000000000004... = 
0b0011111111101001100110011001100110011001100110011001100110011010

1
投票

通常,当您向较大的总和添加小增量时会出现问题。没有足够的精度来存储完整的结果,并且失去了一些重要性。通过循环的最后一次迭代,你已经开始遇到这种情况。

对于足够大的总和和小的增量,总和可能根本不会改变。


1
投票

虽然AMA的答案是正确的,因为在每次添加之后都会发生舍入,即使只进行一次操作(包括乘法),也会发生同样的惊喜:

#include <iostream>

int main()
{
     const auto val1 = 0.3444444444444444
              , val2 = 0.34444444444444442;
     std::cout << (2*val1) << '\n'
               << (2*val2) << '\n';
}

(除非另有说明,否则我假设IEEE使用标准舍入行为加倍。)

第一行将显示0.6888888888888888(如果您相信我为您进行计数,输入为15x 4,输出为15x 8),没有任何意外。我们假设第二行显示一个额外的数字,希望有点接近4,或者结果没有变化。

但实际上,第二行将显示为0.6888888888888889。这是一个惊喜,最后一位数字4如何在下一个更高的数字上四舍五入?这与我们的观点相矛盾,即当双方采用正比例因子时,不平等得以维持。即因为2 <2.5,那么2 * 2 <2 * 2.5,然后4 <5。这意味着,因为在2*val2中需要最后一个数字5来向上舍入(在十进制系统中),所以val2在直线上必须至少为0.344444444444444425才能进行向上舍入。

这里的问题是每个数字系统都有不同的输入和输出舍入。事实上,由于乘法本身,二进制甚至不会出现舍入,但是在两个数字系统转换中都会发生舍入。输入的二进制表示:

0.01011000001011011000001011011000001011011000001011001(val1)0.01011000001011011000001011011000001011011000001011011(val2

乘以2只是左移1,当然,二进制,包括浮点(至少如果我们忽略溢出的可能性),所以输出是:

0.10110000010110110000010110110000010110110000010110010(2*val1)0.10110000010110110000010110110000010110110000010110110(2*val2

后者转换回0.68888888888888888395 ...(注意现在还有8个),正确舍入到0.68888888888888889。

在这种特殊情况下,令人惊讶的行为的原因是val2实际上变成:

0.3444444444444444419772821675

还有一个额外的4替换我们输入的尾随2,并且当加倍时,导致向上舍入以十进制发生。

© www.soinside.com 2019 - 2024. All rights reserved.