浮点数运算是否存在问题?

3909

考虑以下代码:

0.1 + 0.2 == 0.3  ->  false
0.1 + 0.2         ->  0.30000000000000004
为什么会出现这些不准确的情况?

215
浮点变量通常会表现出这种行为,这是由于它们在硬件中的存储方式所致。欲了解更多信息,请查阅“浮点数”维基百科文章。 - Ben S
94
JavaScript 将小数视为浮点数,这意味着像加法这样的操作可能会受到舍入误差的影响。您可能需要查看这篇文章:计算机科学家应该了解的浮点运算知识 - matt b
8
仅供参考,JavaScript 中的所有数值类型都是 IEEE-754 双精度浮点数。 - Gary Willoughby
1
@Gary True,虽然您可以保证在处理15位以下的整数时具有完美的整数精度,请参阅http://www.hunlock.com/blogs/The_Complete_Javascript_Number_Reference。 - Ender
16
由于JavaScript使用IEEE 754标准的Math库,因此它使用64位浮点数。这在进行浮点数(十进制)计算时会导致精度误差,简而言之,这是由于计算机工作在Base 2二进制基础上,而十进制是Base 10十进制基础上造成的。 - Pardeep Jain
显示剩余8条评论
34个回答

36

我的解决方法:

function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

精度指的是在加法运算中,你想要保留小数点后的位数。


36
不,不是坏了,但大多数小数必须近似表示。
概要
浮点数算术确实是精确的,不幸的是,它与我们通常的十进制数表示不太匹配,所以我们经常给它输入与我们写的略有不同。
即使是简单的数字,如0.01、0.02、0.03、0.04……0.24,在二进制小数中也无法准确表示。如果你从0.01开始计数,.02、.03……直到0.25,你才会得到第一个可以在二进制中表示的分数。如果你使用浮点数进行计算,你的0.01会有些偏差,所以要将它们加起来得到一个漂亮的准确的0.25,就需要一个涉及守卫位和舍入的长链条的因果关系。这很难预测,所以我们只能举起双手说“浮点数是不精确的”,但这并不完全正确。
我们不断地给浮点硬件一些在十进制中看起来很简单但在二进制中是循环小数的东西。
这是怎么发生的?
当我们用十进制写数字时,每个分数(特别是每个终止小数)都是形如a / (2^n x 5^m)的有理数。
而在二进制中,我们只得到了2^n这一项,即:a / 2^n。
所以在十进制中,我们无法表示1/3。因为十进制包含2作为一个质因数,我们可以将每个可以写成二进制分数的数字也写成十进制分数。然而,我们几乎无法将任何一个十进制分数表示为二进制。在从0.01、0.02、0.03...0.99的范围内,只有三个数字可以用我们的浮点数格式表示:0.25、0.50和0.75,因为它们分别是1/4、1/2和3/4,都是只使用了2^n这一项的质因数的数字。
在十进制中,我们无法表示1/3。但在二进制中,我们无法表示1/10或1/3。
因此,虽然每个二进制小数都可以用十进制表示,但反过来并不成立。实际上,大多数十进制小数在二进制中会重复。
处理这个问题
开发人员通常被指示进行epsilon比较,更好的建议可能是将值四舍五入为整数值(在C库中:round()和roundf(),即保持在FP格式中),然后进行比较。将值四舍五入到特定的十进制小数位数可以解决大多数输出问题。
此外,在实数计算问题中(FP是为早期昂贵的计算机而发明的问题),宇宙的物理常数和所有其他测量只能以相对较小的有效数字知道,因此整个问题空间本来就是“不精确”的。在这种应用中,FP的“准确性”并不是一个问题。
整个问题实际上是当人们试图用浮点数进行计数时才会出现的。它确实适用于这种情况,但前提是你只使用整数值,这有点违背了使用浮点数的初衷。这就是为什么我们有那么多十进制小数软件库的原因。 我喜欢Chris提出的比萨解答,因为它描述了实际问题,而不仅仅是关于“不准确性”的一般性说辞。如果浮点数只是“不准确”,我们早就可以修复它了。之所以没有这样做,是因为浮点数格式紧凑快速,是处理大量数字的最佳方式。此外,它还是太空时代、军备竞赛和早期尝试用非常慢的计算机和小型存储系统解决大问题的遗留物。(有时,甚至使用磁芯进行1位存储,但那是另外一个故事。

结论

如果你只是在银行计算豆子,那么使用十进制字符串表示的软件解决方案完全可以胜任。但你不能用这种方式进行量子色动力学或空气动力学的计算。

2
虽然浮点数是一种“遗留”格式,但它设计得非常好。如果现在重新设计,我不知道有什么人会改变它。我学习得越多,就越认为它真的好设计。例如,偏置指数意味着连续的二进制浮点数具有连续的整数表示,因此您可以使用IEEE浮点数的二进制表示中的整数递增或递减来实现nextafter()。此外,您可以将浮点数作为整数进行比较,并获得正确的答案,除非它们都是负数(因为符号-大小与2的补码)。 - Peter Cordes
我不同意,浮点数应该以十进制而不是二进制形式存储,这样所有问题都可以得到解决。 - Ronen Festinger
“*x / (2^n + 5^n)” 应该改为 “x / (2^n * 5^n)*” 吧? - Wai Ha Lee
1
@RonenFestinger:所有问题?不,即使在存储为十进制浮点数时,根本问题仍然存在,例如(1/3)* 3!= 1在这种格式下。 - President James K. Polk
它比0.1 + 0.2 != 0.3更好。 - Ronen Festinger
显示剩余2条评论

32

已经有很多好答案发布了,但我想补充一个。

并非所有数字都可以通过 floats/doubles 表示。 例如,在IEEE754浮点数标准的单精度下,数字“0.2”将表示为“0.200000003”。

用于存储实数的模型在底层将浮点数表示为

enter image description here

即使您可以轻松键入0.2,但 FLT_RADIXDBL_RADIX 为2;对于使用“IEEE标准二进制浮点算术(ISO / IEEE Std 754-1985)”的FPU计算机而言,不是10。

因此,要准确表示这样的数字有点困难。即使您明确指定此变量而没有任何中间计算。


31

这个著名的双精度问题相关的一些统计数据。

当使用0.1的步长(从0.1到100)将所有值 (a + b) 相加时,存在约15%的精度误差机会。请注意,该误差可能导致略微更大或更小的值。以下是一些示例:

0.1 + 0.2 = 0.30000000000000004 (BIGGER)
0.1 + 0.7 = 0.7999999999999999 (SMALLER)
...
1.7 + 1.9 = 3.5999999999999996 (SMALLER)
1.7 + 2.2 = 3.9000000000000004 (BIGGER)
...
3.2 + 3.6 = 6.800000000000001 (BIGGER)
3.2 + 4.4 = 7.6000000000000005 (BIGGER)

从100到0.1,使用步长为0.1进行减法计算(a - b,其中a > b),我们有约34%的精度误差机会。 以下是一些例子:

0.6 - 0.2 = 0.39999999999999997 (SMALLER)
0.5 - 0.4 = 0.09999999999999998 (SMALLER)
...
2.1 - 0.2 = 1.9000000000000001 (BIGGER)
2.0 - 1.9 = 0.10000000000000009 (BIGGER)
...
100 - 99.9 = 0.09999999999999432 (SMALLER)
100 - 99.8 = 0.20000000000000284 (BIGGER)

*15%和34%确实很大,因此在需要高精度时始终使用BigDecimal。使用2个小数位(步长0.01),情况会更加恶化(18%和36%)。


19
一些高级语言,如Python和Java,配备了工具来克服二进制浮点数的限制。例如:
Python的decimal模块和Java的BigDecimal,内部使用十进制表示数字(而不是二进制表示)。它们都有限的精度,因此仍然容易出错,但是它们解决了大多数二进制浮点算术的常见问题。
在处理货币时,十进制非常好用:十美分加上二十美分总是精确等于三十美分:
>>> 0.1 + 0.2 == 0.3 False >>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3') True Python的decimal模块基于IEEE标准854-1987
Python的fractions模块和Apache Common的BigFraction。它们都将有理数表示为(分子,分母)对,并且它们可能比十进制浮点算术给出更准确的结果。
这两种解决方案都不完美(特别是在性能方面,或者如果我们需要非常高的精度),但它们仍然解决了许多二进制浮点算术的问题。

我们也可以使用定点数。例如,如果您的最小粒度是美分,则可以使用美分数量的整数进行计算,而不是美元。 - qwr

18

你尝试过使用管道胶带解决方案了吗?

尝试确定错误发生的时机,并使用简短的if语句进行修复,虽然不太美观,但对于某些问题来说,这是唯一的解决方案,这也是其中之一。

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

在一个C#科学模拟项目中,我遇到了同样的问题。我可以告诉你,如果你忽略蝴蝶效应,它会变成一条巨大的胖龙并咬你的屁股。


18

出现这些奇怪的数字是因为计算机在计算时使用二进制(基数为2)数制,而我们使用十进制(基数为10)。

大多数的小数无论是在二进制还是十进制中都无法精确表示。结果是一个四舍五入(但精确的)数字。


1
@Nae 我会将第二段翻译为:“大多数分数在十进制或二进制中都无法准确表示。因此,大多数结果将被四舍五入 - 尽管它们仍将精确到所使用的表示中固有的位数/数字。” - Steve Summit

17

这个问题的很多重复提问都是关于浮点数舍入对特定数字的影响。实际上,通过查看感兴趣的计算的精确结果,比仅仅阅读相关内容更容易理解它的工作原理。一些编程语言提供了这样的功能,例如在Java中将 floatdouble 转换为 BigDecimal

由于这是一个与编程语言无关的问题,所以需要使用与编程语言无关的工具,例如十进制到浮点数转换器

将此应用于视为双精度浮点数的问题中的数字:

0.1 转换为 0.1000000000000000055511151231257827021181583404541015625,

0.2 转换为 0.200000000000000011102230246251565404236316680908203125,

0.3 转换为 0.299999999999999988897769753748434595763683319091796875,以及

0.30000000000000004 转换为 0.3000000000000000444089209850062616169452667236328125。

手动添加前两个数字或使用诸如高精度计算器的小数计算器,可以得到实际输入的准确和为 0.3000000000000000166533453693773481063544750213623046875。

如果它被舍入到相当于0.3的值,那么舍入误差将为0.0000000000000000277555756156289135105907917022705078125。如果舍入到相当于0.30000000000000004的值,也会产生舍入误差0.0000000000000000277555756156289135105907917022705078125。采用银行家舍入法。

回到浮点数转换器,0.30000000000000004的原始十六进制为3fd3333333333334,它以偶数结束,因此这是正确的结果。


2
刚才我撤销了你的编辑,我认为引用代码时使用代码引用是合适的。这个回答是与语言无关的,根本没有引用任何代码。数字可以用在英文句子中,但这并不意味着它们就成为了代码。 - Patricia Shanahan
这很可能是为什么有人将您的数字格式化为代码的原因 - 不是为了格式化,而是为了可读性。 - Wai Ha Lee
此外,“四舍六入五成双”是指二进制表示法,而不是十进制表示法。请参见此链接或者例如这个 - Wai Ha Lee
@WaiHaLee 我没有对任何十进制数进行奇偶性测试,只针对十六进制进行了测试。一个十六进制数字的二进制展开式中,最低有效位为零,则该数字为偶数。 - Patricia Shanahan

17

我能不能加一句; 人们总是假设这是计算机问题,但如果你用手数数(十进制),你就无法得到 (1/3+1/3=2/3)=true ,除非你有无限的0.333...加上0.333...,所以就像在二进制中的 (1/10+2/10)!==3/10 问题一样,你将其截断为0.333 + 0.333 = 0.666,可能会四舍五入为0.667,这也是技术上不准确的。

三进制计数,第三个不是问题——也许一些每只手15个手指的民族会问你的小数点数学是怎么回事...


由于人类使用十进制数,我认为没有什么好的理由为什么浮点数不应该默认表示为十进制,这样我们就可以得到准确的结果。 - Ronen Festinger
人类使用许多除了十进制以外的进位制,其中二进制是我们在计算中使用最多的一种。这么做的“好理由”是因为你无法在每个进位制中表示每个分数。 - user1641172
1
@RonenFestinger 二进制算术在计算机上易于实现,因为它只需要八个基本数字操作:假设$a$、$b$属于$0$或$1$,你只需要知道$\operatorname{xor}(a,b)$和$\operatorname{cb}(a,b)$即可,其中xor是异或运算而cb是“进位位”,在所有情况下都是$0$,除非$a=1=b$,此时我们有一个(事实上,所有运算的交换律会节省$2$个案例,所以你只需要$6$条规则)。十进制扩展需要存储$10\times11$(使用十进制表示法)种情况,并且对于每个位需要$10$个不同的状态,并浪费了进位的存储空间。 - Oskar Limka
2
@RonenFestinger - 十进制并不更准确。这就是这个答案所说的。对于您选择的任何基数,都会有可以给出无限重复数字序列的有理数(分数)。值得一提的是,一些最早的计算机确实使用十进制表示数字,但开创性的计算机硬件设计师很快得出结论,使用二进制更容易和更有效率。 - Stephen C

10

数字计算机中可实现的浮点数学必然使用对实数的近似以及对其进行运算。 (标准版本需要50多页文档并有一个委员会处理其勘误和进一步完善。)

这种近似是不同类型的近似混合而成,每种近似方式都可以被忽略或仔细考虑其与精确度的特定偏差。 它还涉及在硬件和软件层面上的许多明确异常情况,大多数人在经过时假装没有注意到。

如果您需要无限精度(例如,使用数字π而不是它的许多较短的替代品之一),则应编写或使用符号数学程序。

但是,如果您认为有时浮点数学在价值和逻辑方面存在模糊性,并且错误可能会迅速累积,并且您可以编写要求并测试以允许该问题,则您的代码通常可以使用您的FPU。


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