考虑以下代码:

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阈值。

其他回答

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

一些高级语言(如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类。两者都将有理数表示为(分子、分母)对,它们可能给出比十进制浮点运算更精确的结果。

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

你试过胶带解决方案了吗?

尝试确定错误发生的时间,并用简短的if语句修复它们,这并不漂亮,但对于某些问题,这是唯一的解决方案,这就是其中之一。

 if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
                    else { return n * 0.1 + 0.000000000000001 ;}    

我在c#的一个科学模拟项目中也遇到过同样的问题,我可以告诉你,如果你忽视蝴蝶效应,它会变成一条大胖龙,咬你一口**

这里的大多数答案都用非常枯燥的技术术语来解决这个问题。我想用正常人能够理解的方式来解决这个问题。

想象一下,你正试图把披萨切成薄片。你有一个机器人披萨切割机,可以将披萨切成两半。它可以将整个披萨减半,也可以将现有的披萨减半,但无论如何,减半总是准确的。

那台披萨切割机动作非常精细,如果你从一整块披萨开始,然后将其减半,然后继续每次将最小的披萨片减半,你可以在披萨片太小甚至无法实现高精度功能之前,将其减半53次。此时,您不能再将非常薄的切片减半,但必须按原样包含或排除它。

现在,你如何将所有的切片以这样一种方式分割,使其达到披萨的十分之一(0.1)或五分之一(0.2)?真的想一想,试着解决它。如果你手边有一个神话般的精密披萨切割机,你甚至可以尝试使用真正的披萨


当然,大多数有经验的程序员都知道真正的答案,那就是,无论你切得多细,都无法用这些切片拼凑出十分之一或五分之一的披萨。你可以做一个非常好的近似值,如果你把0.1的近似值和0.2的近似值相加,你会得到非常好的0.3的近似值。

对于双精度数字(允许您将披萨减半53次的精度),小于或大于0.1的数字分别为0.09999999999999999167332731531132594682276248931884765625和0.1000000000000000055511151231257827021181583404541015625。后者比前者更接近0.1,因此,如果输入值为0.1,数字解析器将倾向于后者。

(这两个数字之间的区别是“最小切片”,我们必须决定是否包含,这会引入向上的偏差,或者排除,这会带来向下的偏差。最小切片的技术术语是ulp。)

在0.2的情况下,数字都是相同的,只是放大了2倍。同样,我们赞成略高于0.2的值。

注意,在这两种情况下,0.1和0.2的近似值都有轻微的向上偏差。如果我们加上足够多的这些偏差,它们会将数字推离我们想要的越来越远,事实上,在0.1+0.2的情况下,偏差足够高,从而导致的数字不再是最接近0.3的数字。

特别是,0.1+0.2实际上是0.1000000000000000055511151231257827021181583404541015625+0.0200000000000000011102230246251565404236316680908203125=0.30000000000000000444089209850062616169452667236328125,而最接近0.3的数字实际上是0.29999999999988897769753748434595763683319091796875。


另外,一些编程语言还提供了披萨切割机,可以将披萨切成十分之一。虽然这种披萨切刀并不常见,但如果你有机会切到一个,那么你应该在切到十分之一或五分之一的披萨片非常重要的时候使用它。

(最初发布在Quora上。)

我可以补充一下吗;人们总是认为这是一个计算机问题,但如果你用手(以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,这在技术上也是不准确的。

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

其实很简单。当你有一个基数为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/