计算机是如何进行浮点运算的?

35
当我编写以下代码时:
```python 0.1 + 0.2 == 0.3 ```
它为什么会返回False?
cout << 1.0 / 3.0 <<endl;
我看到 0.333333,但当我写下

时...
cout << 1.0 / 3.0 + 1.0 / 3.0 + 1.0 / 3.0 << endl;

我看到了1

计算机是如何做到这一点的?请解释这个简单的例子。对我来说足够了。


2
这听起来像是个好消息,不是吗? - Luc M
5
@关闭投票者:我在这个问题中没有看到任何离题或过于宽泛的内容。如果您认为有,请在评论中解释清楚。 - Marc Mutz - mmutz
4
我很惊讶这四个投票者为什么会生气 :)! - Narek
2
顺便说一句,几乎每个答案都错了。大多数答案认为浮点单元的位是错误的,但输出转换会四舍五入到正确的值。这完全不是真的。在十进制和二进制中,有理但重复的分数1/3无法被精确表示,但当它被加两次时,确实会四舍五入到完全正确的答案。很多答案出错的地方:这种四舍五入发生在最后一个操作中,第二个加法运算中,并且它发生在最低有效位(2 **-23或2 **-53)。结果是一个完全精确的1.0(0x3f800000),不管输出转换如何进行。 - DigitalRoss
5个回答

28

我看过这篇文章,其中包含一些定理和证明。看起来并不是太简洁,因此我才提出了这个问题! :) - Narek
9
@Narek:浮点运算并不简单,如果你打算进行一些复杂的操作,那么你一定应该阅读这篇文章。我建议你针对它提出具体问题,而不是一个如此笼统的问题。 - Alexandre C.
2
@Alexandre C,目前我不需要整个理论。我只需要理解这个简单的例子。 - Narek
9
@Narek:您需要理解这个简单的例子。 - Alexandre C.
现在的计算机非常强大,可以将实数表示为以10为基数的整数数组(例如Java中的BigDecimal),这将解决精度问题,例如像0.1这样的数字比2^n + 2^(n-1) + ...更常用,为什么没有强烈的趋势朝这个方向发展呢? - Nutel
显示剩余2条评论

19
问题在于浮点格式将分数表示为二进制。
第一个分数位是1/2,第二个是1/4,以此类推,表示为1/2的n次方。
而问题是,不是每个有理数(可以表示为两个整数之比的数字)在这种二进制格式中都有有限表示。
(这使得浮点格式在货币价值方面难以使用。虽然这些价值总是有理数(n/100),但只有.00、.25、.50和.75实际上在任意数量的二进制小数位中具有确切表示。)
无论如何,当你把它们加起来时,系统最终有机会将结果四舍五入为它可以准确表示的数字。
在某个时候,它发现自己将.666...数加到.333...数上,就像这样:
  00111110 1  .o10101010 10101010 10101011
+ 00111111 0  .10101010 10101010 10101011o
------------------------------------------
  00111111 1 (1).0000000 00000000 0000000x  # the x isn't in the final result

左侧的位是符号位,接下来的八位是指数位,剩余的位是小数位。在指数位和小数位之间,有一个假定的"1"始终存在,因此没有被实际存储为规范化的最左侧小数位。我已经将实际不存在的零用单独的位o表示。
在这里发生了很多事情,在每个步骤中,FPU采取了相当英勇的措施对结果进行四舍五入。保留了两个额外的精度数字(超出结果能够容纳的),并且在许多情况下,FPU知道剩余最右侧的位是否为1或至少为1。如果是,则分数的该部分大于0.5(按比例缩放),因此向上舍入。中间舍入的值使FPU可以将最右侧的位一直带到整数部分,最终舍入到正确的答案。
这不是因为任何人添加了0.5而发生的;FPU只是尽力在格式的限制范围内做得最好。浮点数实际上并不是不准确的。它是完全准确的,但我们在基于10进制和有理数的世界观中期望看到的大多数数字都不能由格式的基于2进制的小数表示。实际上,可以表示的数字非常少。

我想强调的是,1.0答案绝对 不是 输出转换例程中最终四舍五入的结果。实际上,所有当前的FPU单元在这种情况下都会产生精确的1.0 - DigitalRoss

17

让我们来做个数学题。为了简洁起见,我们假设您只有四个有效的(二进制)数字。

当然,由于gcd(2,3)=1,在以二进制表示时,1/3是循环小数。特别地,它无法被精确表示,因此我们需要使用近似值。

A := 1×1/4 + 0×1/8 + 1×1/16 + 1*1/32

比起其他数,哪个更接近于实际值为 1/3 的值?

A' := 1×1/4 + 0×1/8 + 1×1/16 + 0×1/32

因此,将A以十进制打印出来得到0.34375(您在示例中看到0.33333的事实只是证明了double拥有更多有效数字)。

当这些数字相加三次时,我们得到

A + A + A
= ( A + A ) + A
= ( (1/4 + 1/16 + 1/32) + (1/4 + 1/16 + 1/32) ) + (1/4 + 1/16 + 1/32)
= (   1/4 + 1/4 + 1/16 + 1/16 + 1/32 + 1/32   ) + (1/4 + 1/16 + 1/32)
= (      1/2    +     1/8         + 1/16      ) + (1/4 + 1/16 + 1/32)
=        1/2 + 1/4 +  1/8 + 1/16  + 1/16 + O(1/32)

O(1/32)项不能在结果中表示,因此被丢弃,我们得到:

A + A + A = 1/2 + 1/4 + 1/8 + 1/16 + 1/16 = 1

证毕 :)


2
但事实上,结果恰好为1.0只是运气而已。它们很容易偏离1或2个LSB,而他的程序仍然会显示1,因为默认情况下,浮点转换的精度只有6。 - James Kanze
1
是的,这个答案是不正确的,但正确结果的原因并非“运气”。事实上,所有三个分数的加和将会产生一个精确的1.0,无论你打印多少位小数。 - DigitalRoss
@DigitalRoss:能否解释一下“这个答案是错误的”? - Marc Mutz - mmutz
@UpAndAdam 请再读一遍。核心问题在于结果的O(1/32)部分由于有限数量的有效二进制数字而无法在FP类型中表示,从而产生精确的1.0。 - Marc Mutz - mmutz

2

对于这个具体的例子:我认为现在编译器太聪明了,如果可能的话,会自动确保原始类型的const结果是精确的。我还没有成功地欺骗g ++进行像这样简单的计算。

但是,通过使用非常量变量很容易绕过这些限制。仍然,

int d = 3;
float a = 1./d;
std::cout << d*a;

将会准确产生1,尽管这不应该真正被期望。原因就像已经说过的那样,是因为operator<<会舍去误差。

至于为什么它可以这样做:当你加上相似大小的数字或将float乘以int时,你几乎可以获得浮点类型最大可能提供的所有精度——也就是说,比率误差/结果非常小(换句话说,假设你有一个正误差,错误发生在后面的小数位)。

所以3*(1./3),即使作为浮点数,不完全等于==1,但具有很大的正确偏差,这阻止了operator<<处理小误差。然而,如果你通过减去1来消除这个偏差,浮点数将会滑到错误处,突然间它变得不可忽略了。正如我所说,如果你只是键入3*(1./3)-1,因为编译器太聪明了,所以这种情况不会发生,但是试一试。

int d = 3;
float a = 1./d;
std::cout << d*a << " - 1 = " <<  d*a - 1 << " ???\n";

我得到的结果(g++,32位Linux)是

1 - 1 = 2.98023e-08 ???

我使用Linux 64位和g++编译器,却得到了0。但我真的很想知道如何找到错误? - Narek
很有趣。我没有“预料”它在32位和64位上的工作方式不同,但我也不确定... - leftaroundabout
你尝试过使用比3更大的质数吗?嵌套分数呢? - leftaroundabout
1
你正在使用不同版本的g++和/或不同的优化级别。尝试将其设置为extern int d; extern float a,并编译一个仅包含int d = 3; float a = 1./d;的单独源文件,然后将其链接到可执行文件中。这应该可以避免任何精度损坏的优化。 - Chris Dodd

0
这个代码能够正常工作是因为默认精度为6位小数,而将结果四舍五入到6位小数后得到了1。详见C++草案标准(n3092)中的27.5.4.1 basic_ios构造函数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接