考虑以下代码:

0.1 + 0.2 == 0.3  ->  false
0.1 + 0.2         ->  0.30000000000000004

为什么会出现这些错误?


当前回答

二进制浮点数学是这样的。在大多数编程语言中,它基于IEEE 754标准。问题的关键在于,数字以这种格式表示为整数乘以2的幂;分母不是2的幂的有理数(如0.1,即1/10)无法精确表示。

对于标准binary64格式的0.1,表示形式可以完全写为

0.1000000000000000055511151231257827021181583404541015625(十进制),或0x1.999999999999ap-4,采用C99六进制浮点数表示法。

相比之下,有理数0.1(1/10)可以完全写成

0.1(十进制),或0x1.999999999999999…p-4,类似于C99十六进制浮点数,其中。。。表示9的无限序列。

程序中的常数0.2和0.3也将近似于其真实值。恰好最接近0.2的两倍大于有理数0.2,但最接近0.3的两倍小于有理数0.3。0.1和0.2的和最终大于有理数0.3,因此与代码中的常数不一致。

浮点运算问题的一个相当全面的处理是每个计算机科学家都应该知道的浮点运算。有关更容易理解的解释,请参阅floatingpoint-gui.de。

边注:所有位置(以N为基数)数字系统都有精度问题

普通的十进制(以10为基数)数字也有同样的问题,这就是为什么像1/3这样的数字最终会变成0.33333333。。。

您刚刚偶然发现了一个数字(3/10),它很容易用十进制表示,但不适合二进制。它也是双向的(在某种程度上):1/16在十进制中是一个丑陋的数字(0.0625),但在二进制中,它看起来和十进制中的第10000个一样整洁(0.0001)**-如果我们在日常生活中习惯使用基数为2的数字系统,你甚至会看着这个数字,本能地理解你可以通过将某个数字减半,一次又一次地减半来达到这个目的。

当然,这并不是浮点数在内存中的存储方式(它们使用了一种科学的表示法)。然而,它确实说明了一点,二进制浮点精度错误往往会出现,因为我们通常感兴趣的“真实世界”数字往往是十的幂,但这只是因为我们每天使用十进制数字系统。这也是为什么我们会说71%而不是“每7取5”(71%是一个近似值,因为5/7不能用任何小数精确表示)。

所以不:二进制浮点数并没有被破坏,它们只是碰巧和其他N进制一样不完美:)

边注:在编程中使用浮点

实际上,这种精度问题意味着在显示浮点数之前,需要使用舍入函数将浮点数舍入到您感兴趣的小数位数。

您还需要用允许一定公差的比较来替换相等测试,这意味着:

如果(x==y){…}则不执行

相反,如果(abs(x-y)<myToleranceValue){…},则执行此操作。

其中abs是绝对值。需要为您的特定应用程序选择myToleranceValue,这与您准备允许多少“摆动空间”以及您将要比较的最大值(由于精度损失问题)有很大关系。当心您选择的语言中的“epsilon”样式常量。这些值可以用作公差值,但它们的有效性取决于您使用的数字的大小,因为使用大数字的计算可能会超过epsilon阈值。

其他回答

正常的算术是以10为基数的,所以小数表示十分、百分等。当你试图用二进制2为基数的算术表示浮点数时,你要处理的是半、四、八等。

在硬件中,浮点存储为整数尾数和指数。尾数表示有效数字。指数类似于科学记数法,但它使用的基数是2而不是10。例如,64.0将用尾数1和指数6表示。0.125将用尾数1和指数-3表示。

浮点小数必须加上2的负幂

0.1b = 0.5d
0.01b = 0.25d
0.001b = 0.125d
0.0001b = 0.0625d
0.00001b = 0.03125d

等等

在处理浮点运算时,通常使用误差增量而不是相等运算符。而不是

if(a==b) ...

你会使用

delta = 0.0001; // or some arbitrarily small amount
if(a - b > -delta && a - b < delta) ...

我可以补充一下吗;人们总是认为这是一个计算机问题,但如果你用手(以10为基数)计算,你就不能得到(1/3+1/3=2/3)=真,除非你有无穷大可以将0.333…加到0.333……就像(1/10+2/10)一样==基数2的3/10问题,您将其截断为0.333+0.333=0.666,并可能将其舍入为0.667,这在技术上也是不准确的。

用三进制数,三分之三不是问题——也许有人会问为什么你的十进制数学被打破了。。。

除了其他正确答案之外,您可能还需要考虑缩放值以避免浮点运算的问题。

例如:

var result = 1.0 + 2.0;     // result === 3.0 returns true

…而不是:

var result = 0.1 + 0.2;     // result === 0.3 returns false

在JavaScript中,表达式0.1+0.2===0.3返回false,但幸运的是,浮点中的整数运算是精确的,因此可以通过缩放来避免十进制表示错误。

作为一个实际的例子,为了避免精度至关重要的浮点问题,建议1将钱作为一个整数来处理:2550美分而不是25.50美元。


1 Douglas Crockford:JavaScript:好的部分:附录A——糟糕的部分(第105页)。

鉴于没有人提到这一点。。。

一些高级语言(如Python和Java)提供了克服二进制浮点限制的工具。例如:

Python的十进制模块和Java的BigDecimal类,它们在内部使用十进制表示法(与二进制表示法相反)表示数字。两者都有有限的精度,因此它们仍然容易出错,但它们解决了二进制浮点运算中最常见的问题。小数在处理金钱时很好:10美分加20美分总是正好是30美分:>>> 0.1 + 0.2 == 0.3错误>>>十进制('0.1')+十进制('0.2')==十进制('0.3')真的Python的十进制模块基于IEEE标准854-1987。Python的分数模块和Apache Common的BigFraction类。两者都将有理数表示为(分子、分母)对,它们可能给出比十进制浮点运算更精确的结果。

这两种解决方案都不是完美的(特别是如果我们考虑性能,或者如果我们需要非常高的精度),但它们仍然解决了二进制浮点运算的大量问题。

其实很简单。当你有一个基数为10的系统(像我们的系统)时,它只能表示使用基数素因子的分数。10的主要因子是2和5。因此,1/2、1/4、1/5、1/8和1/10都可以清晰地表达,因为分母都使用10的素因子。相比之下,1/3、1/6和1/7都是重复小数,因为它们的分母使用3或7的素因子。在二进制(或基数2)中,唯一的素因子是2。所以你只能清楚地表达分数,它只包含2作为素因子。在二进制中,1/2、1/4、1/8都可以清晰地表示为小数。而1/5或1/10将是重复小数。因此,0.1和0.2(1/10和1/5)虽然在以10为基数的系统中是干净的小数,但在计算机运行的以2为基数的体系中是重复的小数。当你对这些重复的小数进行数学运算时,当你将计算机的以2(二进制)为基数的数字转换为更易于人类阅读的以10为基础的数字时,你最终会留下剩余部分。

从…起https://0.30000000000000004.com/