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

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个回答

2932
二进制浮点数数学就像这样。在大多数编程语言中,它基于IEEE 754标准。问题的关键在于,使用这种格式表示数字为整数乘以二的幂;分数(例如0.1,即1/10)其分母不是二的幂无法精确表示。
对于标准的binary64格式中的0.1,表示可以精确地写成:
  • 0.1000000000000000055511151231257827021181583404541015625(十进制),或
  • 0x1.999999999999ap-4C99 hexfloat notation)。
相比之下,有理数0.1,即1/10,可以精确地写成:
  • 0.1(十进制),或
  • 0x1.99999999999999...p-4类似于C99 hexfloat表示法的模拟,其中...表示无尽的9序列。
你的程序中的常量0.2和0.3也是它们真实值的近似值。恰巧,最接近0.2的double比有理数0.2大,但最接近0.3的double比有理数0.3小。0.1和0.2的和最终比有理数0.3大,因此与代码中的常量不符。
浮点运算问题的相当全面的处理方法是《计算机科学家应该知道的浮点运算》。更易于理解的解释请参见floating-point-gui.de

旁注:所有位置(基于N)的数字系统都存在这个精度问题

普通的十进制(基于10)数字也有同样的问题,这就是为什么1/3这样的数字最终变成了0.333333333...
你刚刚遇到了一个小数(3/10),它在十进制系统中很容易表示,但在二进制系统中则无法表示。反过来也是一样的(在某种程度上):1/16在十进制中是一个丑陋的数字(0.0625),但在二进制中看起来像十进制中的万分之一一样整洁(0.0001) - 如果我们习惯于在日常生活中使用二进制数系统,您甚至会看着那个数字并本能地理解您可以通过不断折半来到达那里。
当然,这并不是浮点数在内存中存储的方式(它们使用一种科学记数法)。然而,它确实说明了这样一个观点:二进制浮点精度误差往往会出现,因为我们通常感兴趣的“真实世界”数字往往是十的幂 - 但仅仅是因为我们日常使用的是十进制数系统。这也是为什么我们会说71%而不是“每7个中的5个”(71%是一个近似值,因为5/7不能用任何十进制数准确表示)。
所以不,二进制浮点数没有失效,它们只是像其他任何基数N数系统一样不完美 :)

附注:在编程中使用浮点数

实际上,这个精度问题意味着您需要使用舍入函数将浮点数舍入到您感兴趣的小数位数,然后再显示它们。

您还需要用允许一定容差量的比较来替换等式测试,也就是说:

不要写成这样if (x == y) { ... },而应该写成if (abs(x - y) < myToleranceValue) { ... }。其中abs是绝对值函数,myToleranceValue需要根据你的具体应用程序来选择,这将与你准备允许多少"摇摆空间"以及你要比较的最大数字有很大关系(由于精度问题)。在你选择的语言中小心使用"epsilon"样式常量。这些可以用作容差值,但它们的有效性取决于你正在处理的数字的大小,因为大数计算可能会超过epsilon阈值。

213
我认为“某个错误常数”比“Epsilon”更正确,因为并不存在一种可适用于所有情况的“Epsilon”。不同的情况需要使用不同的 Epsilon。而机器 Epsilon 几乎从来不是一个好的常数选择。 - Rotsor
36
并不是所有的浮点数运算都基于IEEE [754]标准,这并不完全正确。例如,仍有一些使用旧的IBM十六进制FP的系统,还有一些图形卡不支持IEEE-754算术。然而,这个说法在相当程度上是正确的。 - Stephen Canon
21
Cray为了速度放弃了IEEE-754的合规性。 Java也松散了其遵循标准以进行优化。 - Art Taylor
37
我认为你应该在这个答案中补充一些内容,关于如何计算货币时应该始终使用整数的固定点算术,因为货币是量化的。可能有必要在一分钱或其他最小货币单位的微小分数中进行内部会计计算——例如,在将“每月29.99美元”转换为日费率时减少舍入误差——但仍应使用固定点算术。 - zwol
52
有趣的事实:二进制浮点表示中无法精确表示0.1导致了一个臭名昭著的“爱国者导弹软件错误”,在第一次伊拉克战争期间造成28人死亡。 - hdl
显示剩余14条评论

720

一个硬件设计师的视角

我认为我应该加入一个硬件设计师的视角,因为我设计和构建浮点硬件。了解错误的起源可能有助于理解软件中正在发生的事情,最终,我希望这有助于解释为什么浮点错误会发生并似乎随着时间累积。

1. 概述

从工程角度来看,大多数浮点运算都会有一些误差,因为执行浮点计算的硬件只需要具有小于最后一位的一半单位的误差。因此,许多硬件将在仅需要产生小于最后一位的一半单位误差的精度下停止进行单个操作,这在浮点除法中尤其成问题。什么构成单个操作取决于该单位接受多少个操作数。对于大多数人来说,是两个,但有些单位需要3个或更多的操作数。由于这个原因,不能保证重复操作会产生理想的误差,因为随着时间的推移,误差会累积。

2. 标准

大多数处理器遵循IEEE-754标准,但有些使用非规范化或不同的标准。例如,在IEEE-754中有一种非规范化模式,可以以精度为代价表示非常小的浮点数。然而,以下内容将涵盖IEEE-754的规范化模式,这是典型的操作模式。
在IEEE-754标准中,硬件设计人员可以允许任何误差/ epsilon值,只要它小于最后一位的一半,并且结果只需要对一个操作小于最后一位的一半。这就解释了为什么重复操作时错误会累加。对于IEEE-754双精度,这是第54位,因为53位用于表示浮点数的数字部分(规范化),也称为尾数(例如5.3e5中的5.3)。接下来的部分将更详细地介绍各种浮点运算中硬件误差的原因。

3. 除法舍入误差的原因

浮点除法错误的主要原因是用于计算商的除法算法。大多数计算机系统使用乘法逆来计算除法,主要是在Z=X/YZ = X * (1/Y)中。除法是通过迭代计算的,即每个周期计算一些商的位数,直到达到所需的精度,对于IEEE-754标准而言,误差小于最后一位的单位。Y(1/Y)的倒数表称为慢除法中的商选择表(QST),商选择表的位数通常是基数的宽度或每次迭代计算的商的位数加上几个保护位。对于IEEE-754标准,双精度(64位),它将是除数基数的大小加上几个保护位k,其中k>=2。因此,例如,一个 typic al Quotient Selection Table,用于每次计算2位商(基数4)的除法器,将是2+2= 4位(加上一些可选位)。 3.1 除法舍入误差:倒数的近似 在商选择表中,倒数取决于除法方法:例如SRT除法这样的慢速除法,或Goldschmidt除法这样的快速除法;每个条目都根据除法算法进行修改,以尝试产生最低可能的误差。无论如何,所有倒数都是实际倒数的近似值,并引入一定的误差。慢速和快速除法方法都是迭代计算商,即每步计算某些位数的商,然后从被除数中减去结果,并且重复这些步骤,直到误差小于最后一位的一半为止。慢速除法方法在每个步骤中计算固定数量的商位数,通常更便宜,而快速除法方法在每个步骤中计算可变数量的位数,通常更昂贵。除法方法的最重要部分是大多数方法都依赖于对倒数的近似值进行重复乘法,因此容易出现误差。

4.其他操作中的舍入误差:截断

另一个导致所有操作中出现舍入误差的原因是IEEE-754允许的最终答案截断的不同模式。有截断、向零舍入、四舍五入(默认), 向下舍入和向上舍入等方法。所有方法都会在单个操作中引入小于一单位的误差。随着时间和重复操作,截断也会累积到结果误差中。这种截断误差在指数运算中尤其棘手,因为涉及某种形式的重复乘法。

5. 重复操作

由于负责浮点数计算的硬件仅需要在单次操作中产生误差小于最后一位的半个单位的结果,如果不加注意地进行重复操作,则误差将随着时间增长。这就是为什么在需要有界误差的计算中,数学家使用诸如使用IEEE-754的最近偶数位四舍五入方法等方法,因为随着时间的推移,错误更有可能互相抵消,而区间算术结合IEEE 754舍入模式的变化来预测舍入误差并进行纠正。由于与其他舍入模式相比其相对误差较低,所以最近偶数位四舍五入是IEEE-754的默认舍入模式。

请注意,默认的舍入模式是四舍五入到最近的偶数位, 它保证了一个操作中误差小于最后一位的一半。仅使用截断、向上取整和向下取整可能会导致误差大于最后一位的一半但小于最后一位,因此这些模式不建议使用,除非在区间算术中使用。

6. 总结

简而言之,浮点运算中错误的根本原因是硬件中的截断以及在除法中倒数的截断的组合。由于IEEE-754标准仅要求单个操作的误差小于最后一位的一半,因此在重复操作中,浮点误差将累加,除非进行修正。


53
浮点数本身并没有误差,每个浮点值都是准确的。但是大多数(但不是全部)浮点运算会产生不准确的结果。例如,没有任何二进制浮点值可以完全等于1.0/10.0。另一方面,一些操作(例如1.0 + 1.0)则会给出确切的结果。 - Solomon Slow
4
感谢@james large指出这点。我编辑了回复以澄清大多数浮点运算的误差小于1/2 ULP(最后一位可表示的单位)。但也有一些特殊情况,结果可能是精确的(比如加零)。 - KernelPanik
26
“浮点数除法中出现错误的主要原因是用于计算商的除法算法”这句话非常误导人。对于符合IEEE-754标准的除法,浮点数除法的唯一错误原因是结果无法在结果格式中被准确地表示;无论使用哪种算法,都会得到相同的结果。 - Stephen Canon
8
@Matt 很抱歉回复晚了。这基本上是由于资源/时间问题和权衡所致。有一种做长除法/更“正常”的除法的方法,叫做基数为二的SRT除法。然而,这种方法会反复将除数从被除数中移位并减去,并且需要很多个时钟周期,因为它每个时钟周期只计算商的一个二进制位。我们使用倒数表可以在一个时钟周期内计算出更多的商位数,从而做出有效的性能/速度权衡。 - KernelPanik
4
@DigitalRoss,我读了你的回答。它解释了为什么没有二进制浮点数(BFP)可以代表实数0.01。我认为我们在事实上并不持不同意见,只是对如何描述它有所不同。你说0.01的BFP表示是“不精确的”。我说它“不存在”。我认为,当你在计算机中输入“0.01”这个“字符串”时,转换函数会给你一个不精确的“结果”。我的思考方式可能受到过去为没有浮点硬件的机器编写低级数学库的工作的影响。 - Solomon Slow
显示剩余11条评论

638

这里的问题和你在学校学习并日常使用的十进制表示法一样,只不过是针对二进制。

要理解这个问题,想象一下将1/3表示为十进制值。这是不可能准确地完成的!当你在小数点后面写上无限多个“3”时,世界已经末日了,所以我们只能写一些数字,并认为它足够准确。

同样地,在二进制(二进制)中, 1/10 (十进制0.1)不能被表示成一个“十进制”值;小数点后面会出现一个无限循环的模式。这个值是不精确的,因此你不能使用普通的浮点数方法进行精确的计算。就像对于十进制一样,还有其他的值也存在这个问题。


172
很好的简短回答。重复的模式看起来像是0.00011001100110011001100110011001100110011001100110011... - Konstantin Chernov
24
有方法可以得到精确的十进制值,如二进制编码十进制(BCD)或其他形式的十进制数。然而,这些方法较慢(慢得多),需要更多存储空间,不如使用二进制浮点数。例如,压缩的BCD在一个字节中存储2个十进制数字。这意味着在一个字节中有100个可能的值,但实际上只用了256个可能值中的100个,浪费了大约60%的可能值。 - Duncan C
2
@IInspectable,对于浮点运算,以BCD为基础的数学运算比本机二进制浮点数慢数百倍。 - Duncan C
3
@DuncanC 嗯,有些方法可以得到精确的十进制值——例如加法和减法。但是对于除法、乘法等运算,它们与二进制方法存在相同的问题。这就是为什么在会计中使用BCD,因为它主要涉及加法和减法,并且无法处理小于一分钱的任何东西。然而,像 1/3*3 == 1 这样简单的运算在BCD数学中失败(求值为假),就像在纸上使用十进制除法时一样失败。 - Joooeey
11
@DuncanC说:“BCD比二进制浮点数慢得多,这是事实。” - 嗯,是的。除非它不是。我敢肯定有一些体系结构,其中BCD数学至少与IEEE-754浮点数学一样快(甚至更快)。但这并不是重点:如果你需要十进制精度,就不能使用IEEE-754浮点表示法。这样做只会达到一个目的:更快地计算出错误的结果。 - IInspectable
显示剩余5条评论

387

这里的大多数答案都用非常干燥、技术性的术语回答了这个问题。我想用普通人可以理解的方式来回答。

想象一下,你正在尝试切披萨。你有一个机器人披萨刀,可以将披萨切成完全相等的两半。它可以将整个披萨分成两半,也可以将现有的一片披萨切成两半,但无论如何,切割总是精确的。

这个披萨刀具有非常细微的运动能力,如果你从整个披萨开始,然后将其切成两半,并且每次都将最小的那片再次切成两半,你可以在切了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.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125,而最接近0.3的数字实际上是0.299999999999999988897769753748434595763683319091796875。
P.S. 一些编程语言还提供可以将比萨切片分成精确的十分之一的工具。虽然这样的比萨切割器不常见,但如果您有使用它的机会,并且需要准确地获得十分之一或五分之一的比萨片时,请使用它。

(原文发布于Quora。)


5
请注意,有些编程语言可以进行精确计算。其中一个例子是Scheme,例如通过GNU Guile。请参见http://draketo.de/english/exact-math-to-the-rescue - 这些编程语言将数学运算保留为分数直到最后再进行转换。 - Arne Babenhauserheide
5
实际上,很少有主流编程语言内置有理数。像Arne和我这样的Schemer会因此受到宠爱。 - C. K. Young
7
@ArneBabenhauserheide,我认为有必要补充一下,这只适用于有理数。所以,如果你在处理像π这样的无理数时,需要将其存储为π的倍数。当然,任何涉及π的计算都不能表示为精确的十进制数。 - Aidiakapi
14
好的。你要如何编程控制比萨旋转器进行36度旋转?什么是36度?(提示:如果你能够精确地定义它,那么你也可以拥有一个将比萨切成十等分的刀片)。换句话说,你不能只用二进制浮点数表示1/360度或1/10(36度)。 - C. K. Young
16
@connexo 除此之外,“每个白痴”都无法将比萨准确旋转36度。人类太容易出错,无法做到如此精确的事情。 - C. K. Young
显示剩余13条评论

232

浮点取整误差。由于缺少5的质因子,0.1在二进制中不能像十进制中那么精确地表示。就像1/3在十进制中需要无限数量的数字来表示,但在三进制中是“0.1”一样,在二进制中,0.1需要无限数量的数字,而在十进制中则不需要。而计算机没有无限的内存。


25
当表示一个分数时,可以使用两个无限精度整数或引号标记。具体的“二进制”或“十进制”概念使其不可能——这意味着您有一系列二进制/十进制数字,并且在其中某个位置上有一个基数点。为了获得精确的有理结果,我们需要更好的格式。 - Devin Jeanpierre
15
@Pacerier:二进制和十进制浮点数都无法精确存储1/3或1/13。十进制浮点数类型可以精确表示形如M/10^E的值,但在表示大多数其他分数时比同等大小的二进制浮点数不够精确。在许多应用中,拥有任意分数的更高精度比拥有少数“特殊”分数的完美精度更有用。 - supercat
@supercat 在比较 binary64decimal64 的精度时:它们的精度相当可比 - 肯定在每个因素之间相差不到10倍。尽管 decimal64 比 binary64 更加摇晃。 - chux - Reinstate Monica
3
二进制和十进制类型之间的精度差异并不是很大,但十进制类型在最佳情况下和最坏情况下的精度差异为10:1,远大于二进制类型的2:1差异。我很好奇是否有人构建了硬件或编写了软件来有效地操作其中任何一种十进制类型,因为它们似乎都不适合在硬件或软件中实现。 - supercat
1
@DevinJeanpierre 我认为重点在于,“计算机”没有“二进制”或“十进制”的“特定概念”。Pacerier的观点似乎是,正是语言设计者们决定过早地转向“浮点数”,当存储这些数字(例如“0.1”、“0.2”和“0.3”)时,它们不仅可以更精确地存储,而且还可以更节省空间地存储为文本(BCD)。 - Jeff Y

153
My answer is quite long, so I've split it into three sections. Since the question is about floating point mathematics, I've put the emphasis on what the machine actually does. I've also made it specific to double (64 bit) precision, but the argument applies equally to any floating point arithmetic.
Preamble:
An IEEE 754 double-precision binary floating-point format (binary64) number represents a number of the form
value = (-1)^s * (1.m51m50...m2m1m0)2 * 2^(e-1023)
in 64 bits:
  • 第一个比特位是符号位:如果数字为负数,则为1,否则为01
  • 接下来的11个比特位是指数,它被1023所偏移。换句话说,在从双精度数字中读取指数位之后,必须减去1023才能得到2的幂。
  • 剩余的52个比特位是尾数(或有效数字)。在尾数中,“隐含”的1.始终被省略,因为任何二进制值的最高有效位都是12

1 - IEEE 754允许有有符号零的概念 - +0-0 被区别对待: 1 / (+0) 为正无穷;1 / (-0) 为负无穷。对于零值,尾数和指数位都为零。注意:零值(+0和-0)明确不被归类为非规格化数2

2 - 对于非规格化数,情况并非如此,它们具有零的偏移指数(和一个隐含的0.)。双精度非规格化数的范围是dmin ≤ |x| ≤ dmax,其中dmin(最小可表示的非零数)为2-1023-51(≈ 4.94 * 10-324),而dmax(最大的非规格化数,其尾数完全由1组成)是2-1023 + 1 - 2-1023 - 51(≈ 2.225 * 10-308)。


将双精度数转换为二进制

有许多在线转换器可以将双精度浮点数转换为二进制(例如在binaryconvert.com),但以下是一些示例C#代码,用于获取双精度数的IEEE 754表示形式(我使用冒号(:)分隔三个部分):

public static string BinaryRepresentation(double value)
{
    long valueInLongType = BitConverter.DoubleToInt64Bits(value);
    string bits = Convert.ToString(valueInLongType, 2);
    string leadingZeros = new string('0', 64 - bits.Length);
    string binaryRepresentation = leadingZeros + bits;

    string sign = binaryRepresentation[0].ToString();
    string exponent = binaryRepresentation.Substring(1, 11);
    string mantissa = binaryRepresentation.Substring(12);

    return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}

直入主题:原始问题

(如果想看简短版请跳到文末)

Cato Johnston(提问者)询问为什么 0.1 + 0.2 不等于 0.3。

以二进制格式书写(用冒号分隔三个部分),这些数值的IEEE 754表示如下:

0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010

请注意,尾数由循环数字0011组成。这是为什么计算存在误差的关键-0.1、0.2和0.3不能像1/9、1/3或1/7一样在有限数量的二进制位中精确表示。
另外需要注意的是,我们可以通过将指数中的幂值减少52并将二进制表示中的小数点向右移动52个位置(类似于10的-3次方*1.23==10的-5次方*123)来实现。这使我们能够以a*2的p次方的形式表示二进制表示所代表的确切值,其中'a'是整数。
将指数转换为十进制数,去除偏移量,并重新添加隐含的1(用方括号表示),0.1和0.2分别为:
0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010
or
0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125

要相加两个数,指数需要相同,即:

0.1 => 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum =  2^-3 * 10.0110011001100110011001100110011001100110011001100111
or
0.1 => 2^-55 * 3602879701896397  = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794  = 0.200000000000000011102230246251565404236316680908203125
sum =  2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875

由于这个总和不是形如2n * 1.{bbb}的形式,我们需要将指数增加一,并移动小数点(二进制点)以得到:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)
    = 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875

现在尾数中有53位(第53位在上一行中用方括号标出)。IEEE 754的默认rounding mode是“四舍五入到最近的值”-即,如果一个数字x介于两个值ab之间,则选择最低有效位为零的值。请注意,保留HTML标记。
a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
  = 2^-2  * 1.0011001100110011001100110011001100110011001100110011

x = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)

b = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
  = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

请注意,ab只在最后一位上不同;...0011 + 1 = ...0100。在这种情况下,最低有效位为零的值是b,因此总和为:
sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
    = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

而0.3的二进制表示为:

0.3 => 2^-2  * 1.0011001100110011001100110011001100110011001100110011
    =  2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875

这段文字讲的是一个数值问题。0.1和0.2的二进制表示是IEEE 754标准下最精确的表示方法,但它们相加后,由于默认的舍入方式,结果只在最低有效位上存在微小差异,差异量为2的-54次方。

简而言之:

将0.1和0.2用IEEE 754二进制表示法相加(用冒号分隔三个部分),并将其与0.3进行比较,可以发现它们只有微小的差异(不同的位用方括号括起来)。

0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3       => 0:01111111101:0011001100110011001100110011001100110011001100110[011]

转换回十进制,这些值是:

0.1 + 0.2 => 0.300000000000000044408920985006...
0.3       => 0.299999999999999988897769753748...

差异正好为2的负54次方,约为5.5511151231258 × 10的负17次方 - 在与原始值相比时微不足道(对于许多应用程序而言)。
比较浮点数的最后几位本质上是危险的,任何阅读过著名的“计算机科学家应该知道的浮点数算术知识”(涵盖了本答案的所有主要部分)的人都会知道。
大多数计算器使用额外的保护位来解决这个问题,这就是为什么0.1 + 0.2会得到0.3的原因:最后几位四舍五入。

141

除了其他正确的答案,您可能还希望考虑将值缩放以避免浮点算术问题。

例如:

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,但幸运的是,在浮点数的整数算术中是精确的,因此可以通过缩放来避免十进制表示错误。

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


1道格拉斯·克罗克福德:JavaScript语言精粹:附录A - 糟糕的部分(第105页)


7
问题在于转换本身存在不准确性。16.08 * 100 = 1607.9999999999998。我们是否需要将数字拆分并单独进行转换(例如,将16 * 100 + 08 = 1608)? - Jason
45
在这里的解决方法是,在进行所有计算时使用整数,然后除以比例(在本例中为100),仅在呈现数据时四舍五入。这将确保您的计算始终是精确的。 - Just a guy
19
稍微挑刺一下:整数运算在浮点数中只有到某个点才是精确的(双关语)。如果数字大于0x1p53(使用Java 7的十六进制浮点表示法,=9007199254740992),则此时ulp为2,因此0x1p53 + 1会被舍入为0x1p53(并且由于舍入至偶数,0x1p53 + 3会被舍入为0x1p53 + 4)。 :-D但是,如果您的数字小于9万亿,则应该没问题。 :-P - C. K. Young

67
计算机中存储的浮点数由两部分组成,一个整数和一个底数的指数,乘以整数部分。
如果计算机是采用十进制,0.1 就是 1 x 10⁻¹,0.2 是 2 x 10⁻¹,0.3 是 3 x 10⁻¹。整数运算简单而精确,所以将 0.1 和 0.2 相加显然得到 0.3。
计算机通常不使用十进制,而是使用二进制。对于一些值仍可以得到精确结果,例如 0.5 是 1 x 2⁻¹,0.25 是 1 x 2⁻²,将它们相加结果为 3 x 2⁻²,即 0.75,完全正确。
问题在于这些十进制下可以被准确表示的数字,在二进制下可能无法准确表示。这些数字需要四舍五入到最接近的等价值。假设采用非常普遍的 IEEE 64 位浮点格式,0.1 的最接近数是 3602879701896397 x 2⁻⁵⁵,0.2 的最接近数是 7205759403792794 x 2⁻⁵⁵;将它们相加得到 10808639105689191 x 2⁻⁵⁵,或确切的小数值 0.3000000000000000444089209850062616169452667236328125。浮点数通常会被舍入以供显示。

2
@Mark 谢谢您清晰的解释,但是问题是为什么0.1+0.4在Python 3中确切地加起来等于0.5。此外,在Python 3中使用浮点数时,检查相等性的最佳方法是什么? - pchegoor
2
@user2417881 IEEE浮点运算对每个操作都有舍入规则,有时候即使两个数相差很小,舍入也能产生精确的答案。细节太长不适合在评论中讨论,而且我本身也不是专家。正如你在这个回答中看到的,0.5是少数几个可以用二进制表示的十进制数之一,但这只是一个巧合。关于相等性测试,请参见https://dev59.com/tG035IYBdhLWcg3wJcjT#33024979。 - Mark Ransom
1
@user2417881,你的问题引起了我的兴趣,所以我将其转化为一个完整的问题和答案:https://dev59.com/f6jja4cB1Zd3GeqP-Gfs - Mark Ransom

61

简而言之,原因如下:

浮点数在二进制中无法精确表示所有小数

就像10除以3在十进制中无法精确表示一样(它将是3.33...循环),同样地,1除以10在二进制中也无法精确表示。

那又怎样?如何处理这个问题? 有没有什么解决办法?

为了提供最佳解决方案,我可以说我发现了以下方法:

parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3

让我解释一下为什么这是最佳解决方案。 正如其他人在上面的答案中提到的,使用现成的JavaScript toFixed()函数来解决问题是个好主意。但很可能你会遇到一些问题。
想象一下,你要将两个浮点数相加,比如0.2和0.7,结果是:0.2 + 0.7 = 0.8999999999999999。
你期望的结果是0.9,也就是说你需要一个保留1位小数的结果。 所以你应该使用(0.2 + 0.7).toFixed(1) 但你不能简单地给toFixed()函数传递一个确定的参数,因为它取决于给定的数字,例如
0.22 + 0.7 = 0.9199999999999999

在这个例子中,你需要保留两位小数,所以应该使用toFixed(2),那么如何设置参数来适应每个给定的浮点数呢?
你可能会说在任何情况下都设为10:
(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"

该死!你打算怎么处理9后面那些不需要的零? 现在是时候将它转换为浮点数,使其符合你的要求。
parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9

既然您找到了解决方案,最好将其提供为如下的函数:

function floatify(number){
           return parseFloat((number).toFixed(10));
        }
    

Let's try it yourself:

function floatify(number){
       return parseFloat((number).toFixed(10));
    }
 
function addUp(){
  var number1 = +$("#number1").val();
  var number2 = +$("#number2").val();
  var unexpectedResult = number1 + number2;
  var expectedResult = floatify(number1 + number2);
  $("#unexpectedResult").text(unexpectedResult);
  $("#expectedResult").text(expectedResult);
}
addUp();
input{
  width: 50px;
}
#expectedResult{
color: green;
}
#unexpectedResult{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +
<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =
<p>Expected Result: <span id="expectedResult"></span></p>
<p>Unexpected Result: <span id="unexpectedResult"></span></p>

你可以这样使用它:
var x = 0.2 + 0.7;
floatify(x);  => Result: 0.9

根据W3SCHOOLS的建议,还有另一种解决方案,你可以通过乘法和除法来解决上述问题。
var x = (0.2 * 10 + 0.1 * 10) / 10;       // x will be 0.3

记住,尽管看起来相同,但 (0.2 + 0.1) * 10 / 10 根本行不通!我更喜欢第一个解决方案,因为我可以将其应用为将输入浮点数转换为准确输出浮点数的函数。 顺便说一下,乘法也存在同样的问题,例如 0.09 * 10 返回 0.8999999999999999。可以使用 floatify 函数作为解决方法: floatify(0.09 * 10) 返回 0.9

1
这让我真的很头疼。我对12个浮点数求和,然后显示它们的总和和平均值。使用toFixed()可能会修复两个数字的求和,但是当求和多个数字时,误差就会显著增加。 - Nuryagdy Mustapayev
@Nuryagdy Mustapayev,我不明白你的意图,因为在我之前测试过,你可以对12个浮点数求和,然后在结果上使用floatify()函数,然后对其进行任何操作,我没有发现任何问题。 - Muhammad Musavi
在我这种情况下,我大约有20个参数和20个公式,每个公式的结果都取决于其它参数的情况下,这个解决方案并没有帮助到我。 - Nuryagdy Mustapayev
1
一些细节:二进制 浮点数无法表示精确的十进制数。使用 十进制 浮点数的系统在这里没有问题(但有其他妥协,尤其是精度和范围比二进制小)。具有本地十进制 fp 的系统包括 IBM z9 和 POWER6 处理器。 - Toby Speight
但是提问者在 0.1 的地方已经出错了。 - undefined

52

浮点数舍入误差。引自《计算机科学家应该知道的浮点运算》

将无限多的实数塞进有限的比特中需要进行近似表示。虽然存在无限多个整数,但在大多数程序中,整数运算的结果可以存储在32位中。相比之下,对于给定任意数量的比特,大多数实数计算都将产生不能用那么多位完全表示的量。因此,浮点计算的结果通常必须进行舍入以适应其有限的表示形式。这种舍入误差是浮点计算的特征。


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