ChatGPT解决这个技术问题 Extra ChatGPT

浮点数学被破坏了吗?

考虑以下代码:

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

为什么会出现这些错误?

浮点变量通常具有这种行为。这是由它们在硬件中的存储方式引起的。有关详细信息,请查看 Wikipedia article on floating point numbers
JavaScript 将小数视为 floating point numbers,这意味着加法等操作可能会出现舍入错误。您可能想看看这篇文章:What Every Computer Scientist Should Know About Floating-Point Arithmetic
仅供参考,javascript 中的所有数字类型都是 IEEE-754 双精度数。
因为 JavaScript 使用 IEEE 754 数学标准,所以它使用 64 位浮点数。这在进行浮点(十进制)计算时会导致精度错误,简而言之,这是由于计算机在 Base 2 中工作,而十进制是 Base 10。
简单解释:1/10是二进制的周期性(0.0 0011 0011 0011...)就像1/3是十进制的周期性(0.333...),所以1/10不能用浮点数准确表示.

2
24 revs, 19 users 27%

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

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

0.10000000000000000055511151231257827021181583404541015625(十进制),或

0x1.999999999999ap-4 C99 hexfloat 表示法。

相反,有理数 0.1,即 1/10,可以完全写为

0.1(十进制),或

0x1.99999999999999...p-4 类似于 C99 hexfloat 表示法,其中 ... 表示 9 的无休止序列。

程序中的常量 0.20.3 也将近似于它们的真实值。碰巧最接近 0.2double 大于有理数 0.2,但最接近 double0.3 小于有理数 0.30.10.2 的总和最终大于有理数 0.3,因此与代码中的常数不一致。

浮点算术问题的一个相当全面的处理是What Every Computer Scientist Should Know About Floating-Point Arithmetic。有关更易于理解的解释,请参见floating-point-gui.de

旁注:所有位置(base-N)数字系统都精确地共享这个问题

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

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

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

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

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

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

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

if (x == y) { ... }

而是执行 if (abs(x - y) < myToleranceValue) { ... }

其中 abs 是绝对值。 myToleranceValue 需要为您的特定应用程序选择 - 这与您准备允许多少“摆动空间”以及您要比较的最大数字可能是多少(由于丢失精度问题)。请注意您选择的语言中的“epsilon”样式常量。这些可用作公差值。


我认为“某个错误常数”比“The Epsilon”更正确,因为没有可以在所有情况下使用的“The Epsilon”。在不同的情况下需要使用不同的 epsilon。并且机器 epsilon 几乎从来都不是一个好用的常数。
并不是所有的浮点数学都基于 IEEE [754] 标准。例如,仍然有一些系统使用旧的 IBM 十六进制 FP,并且仍然有不支持 IEEE-754 算法的显卡。然而,这是一个合理的近似值。
为了速度,Cray 放弃了 IEEE-754 合规性。 Java 也放松了对优化的坚持。
我认为你应该在这个答案中添加一些关于货币计算应该如何始终使用整数的定点算术来完成的内容,因为货币是量化的。 (以美分的一小部分或任何最小的货币单位进行内部会计计算可能是有意义的——这通常有助于减少将“每月 29.99 美元”转换为每日汇率时的舍入误差——但它应该仍然是定点算术。)
有趣的事实:这个 0.1 在二进制浮点中没有精确表示,导致了一个臭名昭著的 Patriot missile software bug,在第一次伊拉克战争中导致 28 人丧生。
V
Vijay S

硬件设计师的观点

我相信我应该为此添加硬件设计师的观点,因为我设计和构建浮点硬件。了解错误的来源可能有助于理解软件中发生的事情,最终,我希望这有助于解释浮点错误发生的原因,并且似乎随着时间的推移而累积。

一、概述

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

2. 标准

大多数处理器遵循 IEEE-754 标准,但有些使用非规范化或不同的标准。例如,IEEE-754 中有一种非规范化模式,它允许以牺牲精度为代价来表示非常小的浮点数。然而,下面将介绍 IEEE-754 的标准化模式,这是典型的操作模式。

在 IEEE-754 标准中,允许硬件设计人员使用任何 error/epsilon 值,只要它小于最后一个单位的二分之一,并且结果只需小于最后一个单位的二分之一一个操作的地方。这就解释了为什么当有重复操作时,错误会加起来。对于 IEEE-754 双精度,这是第 54 位,因为 53 位用于表示浮点数的数字部分(归一化),也称为尾数(例如 5.3e5 中的 5.3)。下一节将更详细地介绍各种浮点运算中硬件错误的原因。

三、除法舍入误差的原因

浮点除法错误的主要原因是用于计算商的除法算法。大多数计算机系统使用乘以逆来计算除法,主要在 Z=X/YZ = X * (1/Y) 中。除法是迭代计算的,即每个周期计算商的一些位,直到达到所需的精度,对于 IEEE-754 来说,这是最后一位误差小于一个单位的任何东西。 Y的倒数表(1/Y)在慢除法中称为商选择表(QST),商选择表的位大小通常是基数的宽度,或位数在每次迭代中计算的商,加上一些保护位。对于 IEEE-754 标准,双精度(64 位),它将是除法器的基数的大小,加上一些保护位 k,其中 k>=2。因此,例如,一次计算 2 位商(基数 4)的除法器的典型商选择表将是 2+2= 4 位(加上一些可选位)。

3.1 除法舍入误差:倒数的近似

商选择表中的倒数取决于division method:慢除法如 SRT 除法,或快速除法如 Goldschmidt 除法;每个条目都根据除法算法进行修改,以尝试产生尽可能低的错误。但是,无论如何,所有倒数都是实际倒数的近似值,并引入了一些误差因素。慢除法和快除法都迭代计算商,即每一步计算商的一些位数,然后从被除数中减去结果,除法器重复这些步骤,直到误差小于一的二分之一单位排在最后。慢除法方法在每个步骤中计算商的固定位数并且通常构建成本较低,而快速除法方法计算每个步骤的可变位数并且通常构建成本更高。除法方法最重要的部分是它们中的大多数依赖于重复乘以一个倒数的近似,因此它们容易出错。

4. 其他运算中的舍入误差:截断

所有操作中舍入误差的另一个原因是 IEEE-754 允许的最终答案截断的不同模式。有截断、向零舍入、round-to-nearest (default), 向下舍入和向上舍入。所有方法都会在单个操作的最后一个位置引入小于一个单位的误差元素。随着时间的推移和重复的操作,截断也会累积地增加结果错误。这种截断误差在求幂中尤其成问题,它涉及某种形式的重复乘法。

5.重复操作

由于进行浮点计算的硬件只需要在单个操作的最后一个位置产生一个误差小于一半的结果,如果不注意,误差会随着重复操作而增长。这就是在需要有界误差的计算中,数学家使用诸如使用 IEEE-754 的四舍五入 even digit in the last place 等方法的原因,因为随着时间的推移,误差更有可能相互抵消,并且Interval ArithmeticIEEE 754 rounding modes 的变体相结合,以预测舍入误差并进行纠正。由于与其他舍入模式相比,它的相对误差较低,舍入到最接近的偶数(在最后一位)是 IEEE-754 的默认舍入模式。

请注意,默认舍入模式,round-to-nearest even digit in the last place,保证一次操作的最后一位的误差小于一个单位的二分之一。单独使用截断、向上舍入和向下舍入可能会导致错误大于最后一位单位的二分之一,但小于最后一位单位,因此不建议使用这些模式,除非它们是用于区间算术。

6.总结

简而言之,浮点运算出错的根本原因是硬件截断和除法倒数截断的结合。由于 IEEE-754 标准只要求单次运算的最后一位误差小于一个单位的二分之一,因此重复运算的浮点误差会累加起来,除非得到纠正。


(3) 错误。一个分部的舍入误差不小于倒数一个单位,最多倒数半个单位。
@gnasher729 好收获。使用默认的 IEEE 舍入模式,大多数基本操作在最后一位的误差也小于一个单位的 1/2。编辑了解释,并且还注意到如果用户覆盖默认舍入模式(在嵌入式系统中尤其如此),错误可能大于 1 ulp 的 1/2 但小于 1 ulp。
(1) 浮点数没有错误。每个浮点值都是它的本来面目。大多数(但不是全部)浮点运算给出不精确的结果。例如,不存在完全等于 1.0/10.0 的二进制浮点值。另一方面,一些操作(例如,1.0 + 1.0)确实给出了准确的结果。
“浮点除法错误的主要原因,是用于计算商的除法算法”是一种非常具有误导性的说法。对于符合 IEEE-754 的除法,浮点除法错误的唯一原因是结果无法以结果格式精确表示;无论使用何种算法,都会计算出相同的结果。
@Matt 抱歉回复晚了。这基本上是由于资源/时间问题和权衡。有一种方法可以进行长除法/更“正常”的除法,它称为 SRT 除法,基数为 2。但是,这会重复移动并从被除数中减去除数,并且需要许多时钟周期,因为它只计算每个时钟周期的商的一位。我们使用倒数表,以便我们可以计算每个周期的商的更多位,并进行有效的性能/速度权衡。
J
Joel Coehoorn

它的破坏方式与您在小学学习并每天使用的十进制(以 10 为底)符号完全相同,仅用于以 2 为底。

要理解,请考虑将 1/3 表示为十进制值。不可能完全做到!世界将在你写完小数点后的 3 之前结束,因此我们写了一些地方并认为它足够准确。

同样,1/10(十进制 0.1)不能以 2 进制(二进制)精确表示为“十进制”值;小数点后的重复模式永远持续下去。该值不准确,因此您无法使用普通浮点方法对其进行精确数学运算。就像以 10 为底的情况一样,还有其他值也表现出这个问题。


伟大而简短的答案。重复模式看起来像 0.00011001100110011001100110011001100110011001100110011 ...
有一些方法可以产生精确的十进制值。 BCD(二进制编码十进制)或各种其他形式的十进制数。然而,这些都比使用二进制浮点更慢(慢很多)并且占用更多的存储空间。 (例如,打包的 BCD 在一个字节中存储 2 个十进制数字。也就是说,一个字节中有 100 个可能的值,实际上可以存储 256 个可能的值,即 100/256,这浪费了大约 60% 的字节可能值。)
@IInspectable,对于浮点运算,基于 BCD 的数学运算比本机二进制浮点慢数百倍。
@DuncanC嗯,有些方法可以产生精确的十进制值——用于加法和减法。对于除法、乘法等,它们与二进制方法具有相同的问题。这就是为什么在会计中使用 BCD 的原因,因为它主要处理加号和减号,你不能解释任何小于一美分的东西。然而,像 1/3*3 == 1 这样简单的东西在 BCD 数学中失败(评估为假),就像在纸上使用十进制除法一样。
@DuncanC:“BCD 比二进制浮点慢很多,句号。” - 嗯,是的。除非不是。很确定有 architectures,其中 BCD 数学至少与 IEEE-754 浮点数学一样快(或更快)。但这不是重点:如果您需要小数精度,则不能使用 IEEE-754 浮点表示。这样做只会实现一件事:更快地计算错误的结果。
C
Chris Jester-Young

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

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

那个比萨刀的动作非常精细,如果你从一整块比萨开始,然后把它减半,然后每次将最小的切片继续减半,你可以减半 53 次,直到切片太小,甚至无法达到高精度的能力.那时,您不能再将那个非常薄的切片减半,而必须按原样包含或排除它。

现在,您将如何将所有切片以这样一种方式拼凑起来,加起来相当于披萨的十分之一 (0.1) 或五分之一 (0.2)?认真想想,然后努力解决。如果您手头有一个神话般的精密比萨刀,您甚至可以尝试使用真正的比萨饼。 :-)

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

对于双精度数字(这是使您可以将披萨减半53倍的精度),该数字立即少于0.1,是0.099999999999999999999999999999167327315315313259468227272727248931893155555555555555555555555555555555555555555555太平洋。后者比前者更接近 0.1,因此数字解析器将在输入 0.1 的情况下支持后者。

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

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

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

In particular, 0.1 + 0.2 is really 0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125, whereas the number closest to 0.3 is actually 0.299999999999999988897769753748434595763683319091796875.

PS 一些编程语言还提供可以 split slices into exact tenths 的比萨刀。虽然这样的比萨刀并不常见,但如果您确实可以使用它,您应该在重要的是能够准确地获得十分之一或五分之一的切片时使用它。

(Originally posted on Quora.)


请注意,有些语言包含精确的数学。一个例子是 Scheme,例如通过 GNU Guile。请参阅 draketo.de/english/exact-math-to-the-rescue — 这些将数学保留为分数,最后只进行切片。
@FloatingRock 实际上,很少有主流编程语言内置有理数。 Arne 和我一样是一名计划者,所以这些都是我们被宠坏的东西。
@ArneBabenhauserheide 我认为值得补充的是,这只适用于有理数。因此,如果您使用 pi 等无理数进行数学运算,则必须将其存储为 pi 的倍数。当然,任何涉及 pi 的计算都不能表示为精确的十进制数。
@connexo 好的。您将如何对您的披萨旋转器进行编程以获得 36 度?什么是36度? (提示:如果你能以精确的方式定义它,你也有一个精确的十分之一的比萨刀。)换句话说,你实际上不可能有 1/360(度数)或 1/ 10(36 度),只有二进制浮点。
@connexo 另外,“每个白痴”都不能将披萨正好旋转 36 度。人类太容易出错,无法做任何如此精确的事情。
D
Devin Jeanpierre

浮点舍入错误。由于缺少 5 的素因子,0.1 在 base-2 中无法像在 base-10 中那样准确地表示。就像 1/3 需要无限位数以十进制表示,但在 base-3 中是“0.1”, 0.1 在 base-2 中采用无限位数,而在 base-10 中则没有。并且计算机没有无限量的内存。


@Pacerier 当然,他们可以使用两个无界精度整数来表示一个分数,或者他们可以使用引号表示法。正是“二进制”或“十进制”的特定概念使这变得不可能——你有一个二进制/十进制数字序列,并且在其中的某个地方有一个小数点。为了获得精确的理性结果,我们需要更好的格式。
@Pacerier:二进制和十进制浮点都不能精确存储 1/3 或 1/13。十进制浮点类型可以精确地表示 M/10^E 形式的值,但在表示大多数其他分数时不如类似大小的二进制浮点数精确。在许多应用程序中,使用任意分数获得更高的精度比使用一些“特殊”分数获得完美的精度更有用。
@supercat 在比较 binary64decimal64 的精度时:精度相当 - 当然彼此相差 10 倍。授予十进制64 比二进制64 摆动更多。
@chux:二进制和十进制类型之间的精度差异并不大,但是十进制类型的最佳情况与最坏情况精度的 10:1 差异远大于二进制类型的 2:1 差异。我很好奇是否有人构建了硬件或编写了软件来有效地在任何一种十进制类型上运行,因为两者似乎都不适合在硬件或软件中有效实现。
@DevinJeanpierre我认为关键是“计算机”没有“'二进制'或'十进制'的特定概念”。 Pacerier 的观点似乎是语言设计者决定过早地跳转到“浮点”,在存储“0.1”、“0.2”和“0.3”这样的数字时,不仅可以更准确,而且也更节省空间地存储为文本 (BCD)。
W
Wai Ha Lee

我的答案很长,所以我把它分成三个部分。由于问题是关于浮点数学的,所以我把重点放在了机器的实际作用上。我还专门针对双精度(64 位)精度,但该参数同样适用于任何浮点运算。

前言

IEEE 754 double-precision binary floating-point format (binary64) 数字表示形式的数字

值 = (-1)^s * (1.m51m50...m2m1m0)2 * 2e-1023

64位:

第一位是符号位:如果数字为负,则为 1,否则为 0。

接下来的 11 位是指数,偏移了 1023。换句话说,从双精度数中读取指数位后,必须减去 1023 才能获得 2 的幂。

剩下的 52 位是有效数(或尾数)。在尾数中,“隐含”的 1. 总是被省略,因为任何二进制值的最高有效位都是 1。

1 - IEEE 754 允许 signed zero 的概念 - +0-0 的处理方式不同:1 / (+0) 是正无穷大; 1 / (-0) 是负无穷大。对于零值,尾数和指数位都为零。注意:零值(+0 和 -0)明确不归类为非正规2

2 - denormal numbers 不是这种情况,它的偏移指数为零(以及隐含的 0.)。非正规双精度数的范围是 dmin ≤ |x| ≤ dmax,其中 dmin(可表示的最小非零数)为 2-1023 - 51 (≈ 4.94 * 10- 324) 和 dmax(最大的非正规数,其尾数完全由 1s 组成)为 2-1023 + 1 - 2< sup>-1023 - 51 (≈ 2.225 * 10-308)。

将双精度数转换为二进制

存在许多在线转换器将双精度浮点数转换为二进制数(例如 binaryconvert.com),但这里有一些示例 C# 代码来获得双精度数的 IEEE 754 表示(我用冒号分隔三个部分({ 2}):

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);
}

切入正题:原始问题

(TL;DR版本跳到底部)

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 * 2p 形式表示的确切值。其中'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 介于两个值 a 之间b,选择最低有效位为零的值。

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 之和的二进制表示仅相差 2-54。

0.1 和 0.2 的二进制表示是 IEEE 754 允许的数字的最准确表示。由于默认的舍入模式,添加这些表示会产生一个仅在最低有效位上有所不同的值。

TL;博士

以 IEEE 754 二进制表示形式写入 0.1 + 0.2(用冒号分隔三部分)并将其与 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 - 与原始值相比(对于许多应用程序而言)微不足道。

比较浮点数的最后几位本质上是危险的,因为任何读过著名的“What Every Computer Scientist Should Know About Floating-Point Arithmetic”(涵盖此答案的所有主要部分)的人都会知道。

大多数计算器使用额外的 guard digits 来解决这个问题,这就是 0.1 + 0.2 给出 0.3 的方式:最后几位被四舍五入。


D
Daniel Vassallo

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

例如:

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

... 代替:

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

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

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

1 道格拉斯·克罗克福德:JavaScript: The Good Parts: Appendix A - Awful Parts (page 105)


问题是转换本身是不准确的。 16.08 * 100 = 1607.9999999999998。我们是否必须求助于拆分数字并单独转换(如 16 * 100 + 08 = 1608)?
此处的解决方案是以整数进行所有计算,然后除以您的比例(在本例中为 100)并仅在呈现数据时四舍五入。这将确保您的计算始终准确。
只是挑剔一点:整数运算仅在浮点数中精确到一个点(双关语)。如果该数字大于 0x1p53(使用 Java 7 的十六进制浮点表示法,= 9007199254740992),则此时 ulp 为 2,因此 0x1p53 + 1 向下舍入为 0x1p53(并且 0x1p53 + 3 向上舍入为 0x1p53 + 4、由于四舍五入)。 :-D 但是当然,如果你的数字小于 9 万亿,你应该没问题。 :-P
M
Mark Ransom

存储在计算机中的浮点数由两部分组成,一个整数和一个指数,以指数为底并乘以整数部分。

如果计算机以 10 为基数工作,则 0.1 将是 1 x 10⁻¹0.2 将是 2 x 10⁻¹0.3 将是 3 x 10⁻¹。整数数学既简单又精确,因此添加 0.1 + 0.2 显然会得到 0.3

计算机通常不以 10 为基数工作,它们以 2 为基数工作。您仍然可以获得某些值的精确结果,例如 0.51 x 2⁻¹0.251 x 2⁻²,并将它们添加到 { 5} 或 0.75。确切地。

问题在于数字可以精确地以 10 为底,但不能以 2 为底。这些数字需要四舍五入到最接近的等值。假设非常常见的 IEEE 64 位浮点格式,最接近 0.1 的数字是 3602879701896397 x 2⁻⁵⁵,最接近 0.2 的数字是 7205759403792794 x 2⁻⁵⁵;将它们加在一起得到 10808639105689191 x 2⁻⁵⁵,或精确的十进制值 0.3000000000000000444089209850062616169452667236328125。浮点数通常四舍五入以便显示。


@Mark感谢您的清晰解释,但随后出现的问题是为什么 0.1+0.4 恰好等于 0.5 (atleast in Python 3) 。在 Python 3 中使用浮点数时检查相等性的最佳方法是什么?
@user2417881 IEEE 浮点运算对每个运算都有舍入规则,有时即使两个数字相差一点,舍入也能产生准确的答案。细节太长了,无法评论,而且我也不是他们的专家。正如您在此答案中看到的那样,0.5 是可以用二进制表示的少数小数之一,但这只是巧合。有关相等性测试,请参阅 stackoverflow.com/questions/5595425/…
@user2417881 你的问题让我很感兴趣,所以我把它变成了一个完整的问答:stackoverflow.com/q/48374522/5987
N
Nae

浮点舍入误差。从 What Every Computer Scientist Should Know About Floating-Point Arithmetic

将无限多个实数压缩为有限位数需要近似表示。尽管整数有无限多,但在大多数程序中,整数计算的结果可以存储在 32 位中。相反,给定任何固定位数,大多数实数计算将产生无法使用那么多位精确表示的量。因此,浮点计算的结果必须经常四舍五入以适应其有限表示。这种舍入误差是浮点计算的特征。


k
kaya3

In short 这是因为:

浮点数不能用二进制精确表示所有小数

因此,就像 10/3 其中 does not exist 精确地以 10 为底(它将是 3.33... 重复),以同样的方式 1/10 在二进制中不存在。

所以呢?如何处理?有什么解决方法吗?

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

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

让我解释一下为什么它是最好的解决方案。正如上面提到的其他人回答的那样,使用现成的 Javascript toFixed() 函数来解决问题是一个好主意。但很可能你会遇到一些问题。

想象一下,您要将两个浮点数相加,例如 0.20.70.2 + 0.7 = 0.8999999999999999

您的预期结果是 0.9,这意味着在这种情况下您需要一个精度为 1 位的结果。所以你应该使用 (0.2 + 0.7).tofixed(1) 但你不能只给 toFixed() 一个特定的参数,因为它取决于给定的数字,例如

0.22 + 0.7 = 0.9199999999999999

在此示例中,您需要 2 位精度,因此它应该是 toFixed(2),那么适合每个给定浮点数的参数应该是什么?

您可能会说在每种情况下都设为 10:

(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"

该死!你打算如何处理 9 点之后的那些不需要的零?现在是时候将其转换为 float 以使其如您所愿:

parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9

现在您找到了解决方案,最好将它作为这样的函数提供:

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

让我们自己尝试一下:

function floatify(number){ return parseFloat((number).toFixed(10)); } 函数 addUp(){ var number1 = +$("#number1").val(); var number2 = +$("#number2").val(); var unexpectedResult = number1 + number2; var expectedResult = floatify(number1 + number2); $("#unexpectedResult").text(unexpectedResult); $("#expectedResult").text(expectedResult); } 加起来();输入{宽度:50px; } #expectedResult{ 颜色:绿色; } #unexpectedResult{ 颜色:红色; } + =

预期结果:

意外结果:

你可以这样使用它:

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 根本不起作用,尽管它看起来一样!我更喜欢第一个解决方案,因为我可以将它用作将输入浮点数转换为准确输出浮点数的函数。


这让我很头疼。我将 12 个浮点数相加,然后显示这些数字的总和和平均值。使用 toFixed() 可能会修复 2 个数字的总和,但是当对多个数字求和时,飞跃很重要。
@Nuryagdy Mustapayev 我没有得到你的意图,因为我在你可以对 12 个浮点数求和之前进行了测试,然后对结果使用 floatify() 函数,然后做任何你想做的事情,我发现使用它没有问题。
我只是说在我的情况下,我有大约 20 个参数和 20 个公式,其中每个公式的结果取决于其他公式,这个解决方案没有帮助。
J
Justineo

我的解决方法:

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

精度是指在加法过程中要在小数点后保留的位数。


D
DigitalRoss

不,没有损坏,但大多数小数必须近似

概括

浮点运算是精确的,不幸的是,它与我们通常的以 10 为基数的数字表示不匹配,所以事实证明我们经常给它输入与我们编写的内容略有不同的输入。

即使是像 0.01、0.02、0.03、0.04 ... 0.24 这样的简单数字也不能完全表示为二进制分数。如果你数到 0.01, .02, .03 ...,直到你达到 0.25,你才会得到 base2 中可表示的第一个分数。如果您尝试使用 FP,您的 0.01 会稍微偏离,因此将其中的 25 个添加到精确的 0.25 的唯一方法将需要涉及保护位和舍入的一长串因果关系。很难预测,所以我们举手说“FP 不准确”,但这不是真的。

我们不断地为 FP 硬件提供一些以 10 为底的看似简单但以 2 为底的重复分数。

这怎么发生的?

当我们用十进制写时,每个分数(特别是每个终止小数)都是形式的有理数

一个 / (2n x 5m)

在二进制中,我们只得到 2n 项,即:

一个/2n

所以在十进制中,我们不能表示 1/3。因为以 10 为底包含 2 作为质因数,所以我们可以写为二进制分数的每个数字也可以写为以 10 为底的分数。然而,几乎我们写为 base10 分数的任何东西都可以用二进制表示。在 0.01、0.02、0.03 ... 0.99 的范围内,我们的 FP 格式只能表示三个数字:0.25、0.50 和 0.75,因为它们是 1/4、1/2 和 3/4,所有数字仅使用 2n 项的质因子。

在 base10 中,我们不能代表 1/3。但在二进制中,我们不能做 1/10 或 1/3。

因此,虽然每个二进制分数都可以写成十进制,但反之则不然。事实上,大多数小数部分都以二进制形式重复。

处理它

开发人员通常被指示进行 < epsilon 比较,更好的建议可能是四舍五入到整数值(在 C 库中:round() 和 roundf(),即保持 FP 格式)然后进行比较。舍入到特定的小数长度可以解决大多数输出问题。

此外,在实数运算问题(FP 是在早期、非常昂贵的计算机上发明的问题)中,宇宙的物理常数和所有其他测量只有相对少量的有效数字知道,所以整个问题空间无论如何是“不精确的”。 FP“准确性”在这种应用程序中不是问题。

当人们尝试使用 FP 进行豆类计数时,整个问题就真正出现了。它确实适用,但前提是你坚持使用整数值,这会破坏使用它的意义。这就是我们拥有所有这些小数部分软件库的原因。

我喜欢 Chris 的 Pizza 答案,因为它描述了实际问题,而不仅仅是关于“不准确”的通常挥手致意。如果 FP 只是“不准确”,我们可以修复这个问题,并且在几十年前就已经做到了。我们没有这样做的原因是因为 FP 格式紧凑且快速,它是处理大量数字的最佳方式。此外,它是太空时代和军备竞赛以及早期尝试使用小内存系统解决非常慢的计算机的大问题的遗产。 (有时,单个 磁芯 用于 1 位存储,但那是 another story.

结论

如果您只是在银行数豆子,那么首先使用十进制字符串表示的软件解决方案效果很好。但是你不能那样做量子色动力学或空气动力学。


在所有情况下,舍入到最接近的整数并不是解决比较问题的安全方法。 0.4999998 和 0.500001 舍入为不同的整数,因此每个舍入切点周围都有一个“危险区”。 (我知道那些十进制字符串可能不能完全表示为 IEEE 二进制浮点数。)
此外,即使浮点是一种“传统”格式,它的设计也非常好。如果现在重新设计它,我不知道任何人会改变它。我对它了解得越多,我就越觉得它的设计真的很好。例如,有偏指数意味着连续的二进制浮点数具有连续的整数表示,因此您可以在 IEEE 浮点数的二进制表示上使用整数递增或递减来实现 nextafter()。此外,您可以将浮点数作为整数进行比较并得到正确的答案,除非它们都是负数(因为符号幅度与 2 的补码)。
我不同意,浮点数应该存储为小数而不是二进制,所有问题都解决了。
“x / (2^n + 5^n)”不应该是“x / (2^n * 5^n)”吗?
@stephen c 您将能够在编译器设置中定义所需的精度。但它只会对结果进行四舍五入,就像在计算器中一样。
K
Konstantin Burlachenko

已经发布了很多好的答案,但我想再附加一个。

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

引擎盖下存储实数的模型将浮点数表示为

https://i.stack.imgur.com/BRvSp.png

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

所以要准确地表示这些数字有点困难。即使您在没有任何中间计算的情况下明确指定此变量。


K
Kostas Kryptos

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

当使用 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)

当使用 0.1(从 100 到 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%)。


A
Andrea Corbellini

鉴于没有人提到这...

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

Python 的十进制模块和 Java 的 BigDecimal 类,它们在内部用十进制表示法(与二进制表示法相反)表示数字。两者都具有有限的精度,因此它们仍然容易出错,但是它们解决了二进制浮点运算的大多数常见问题。处理金钱时,小数非常好:十美分加二十美分总是正好是三十美分:>>> 0.1 + 0.2 == 0.3 False >>> Decimal('0.1') + Decimal('0.2') == Decimal( '0.3') True Python 的十进制模块基于 IEEE 标准 854-1987。

Python 的分数模块和 Apache Common 的 BigFraction 类。两者都将有理数表示为(分子,分母)对,并且它们可能比十进制浮点算术提供更准确的结果。

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


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

您是否尝试过胶带解决方案?

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

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

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


P
Piyush S528

出现这些奇怪的数字是因为计算机使用二进制(以 2 为底)数字系统进行计算,而我们使用十进制(以 10 为底)。

大多数小数不能用二进制或十进制或两者精确表示。结果 - 四舍五入(但精确)的数字结果。


@Nae我会将第二段翻译为“大多数分数不能用十进制或二进制精确表示。所以大多数结果将被四舍五入——尽管它们仍然精确到表示中固有的位数/位数正在使用。”
P
Patricia Shanahan

这个问题的许多重复项中的许多都询问浮点舍入对特定数字的影响。在实践中,通过查看感兴趣的计算的确切结果而不是仅仅阅读它更容易了解它是如何工作的。某些语言提供了执行此操作的方法 - 例如在 Java 中将 floatdouble 转换为 BigDecimal

由于这是一个与语言无关的问题,因此需要与语言无关的工具,例如 Decimal to Floating-Point Converter

将其应用于问题中的数字,视为双打:

0.1 转换为 0.1000000000000000055511151231257827021181583404541015625,

0.2 转换为 0.200000000000000011102230246251565404236316680908203125,

0.3 转换为 0.299999999999999988897769753748434595763683319091796875,和

0.30000000000000004 转换为 0.3000000000000000444089209850062616169452667236328125。

手动添加前两个数字或在十进制计算器(如 Full Precision Calculator)中显示实际输入的确切总和为 0.3000000000000000166533453693773481063544750213623046875。

如果四舍五入到相当于 0.3 的舍入误差将是 0.0000000000000000277555756156289135105907917022705078125。舍入到 0.300000000000000004 的等效值也会给出舍入误差 0.0000000000000000277555756156289135105907917022705078125。圆对平的决胜局适用。

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


对于我刚刚回滚编辑的人:我认为代码引号适合引用代码。这个答案与语言无关,根本不包含任何引用的代码。数字可以用在英语句子中,这不会把它们变成代码。
This 可能是有人将您的数字格式化为代码的原因 - 不是为了格式化,而是为了可读性。
...另外,round to even 指的是 binary 表示,not decimal 表示。请参阅 this 或例如 this
@WaiHaLee我没有将奇数/偶数测试应用于任何十进制数,只有十六进制数。当且仅当其二进制扩展的最低有效位为零时,十六进制数字是偶数。
佚名

我可以添加吗?人们总是认为这是一个计算机问题,但是如果你用手数(以 10 为底),除非你有无穷大将 0.333... 加到 0.333... 上,否则你不能得到 (1/3+1/3=2/3)=true (1/10+2/10)!==3/10 以 2 为底的问题,您将其截断为 0.333 + 0.333 = 0.666 并可能将其四舍五入为 0.667,这在技术上也是不准确的。

以三进制数数,但三分之二不是问题 - 也许一些每只手有 15 个手指的比赛会问为什么你的十进制数学被打破了......


由于人类使用十进制数字,我看不出浮点数默认不表示为小数的充分理由,因此我们有准确的结果。
人类使用除基数 10(十进制)以外的许多基数,二进制是我们最常用于计算的基数。“充分的理由”是您根本无法表示每个基数中的每个分数。
@RonenFestinger 二进制算术很容易在计算机上实现,因为它只需要八个带有数字的基本操作:比如 $0,1$ 中的 $a$, $b$ 您只需要知道 $\operatorname{xor}(a,b) $ 和 $\operatorname{cb}(a,b)$,其中 xor 是异或,cb 是“进位”,在所有情况下都是 $0$,除非 $a=1=b$,在这种情况下我们有一个(实际上所有操作的交换性为您节省了 2 美元的案例,而您所需要的只是 6 美元的规则)。十进制扩展需要存储 10 美元乘以 11 美元(十进制表示法)的情况,每个位需要 10 美元的不同状态,并且在进位上浪费存储空间。
@RonenFestinger - 十进制并不更准确。这就是这个答案的意思。对于您选择的任何基数,都会有有理数(分数)给出无限重复的数字序列。根据记录,一些第一台计算机确实使用以 10 为基数的数字表示,但开创性的计算机硬件设计人员很快得出结论,以 2 为基数更容易实现,效率更高。
a
agc

可以在数字计算机中实现的那种浮点数学必然使用实数的近似值和对它们的运算。 (标准版本有超过 50 页的文档,并有一个委员会来处理其勘误和进一步完善。)

这种近似是不同种类的近似的混合,由于其与精确度的特定偏差方式,每个近似都可以被忽略或仔细考虑。它还涉及许多硬件和软件级别的明显异常情况,大多数人会假装没有注意到。

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

但是,如果您认为有时浮点数学在值和逻辑上是模糊的,并且错误会迅速累积,并且您可以编写您的需求和测试以实现这一点,那么您的代码可以经常通过你的 FPU。


N
Nae

只是为了好玩,我按照标准 C99 中的定义玩弄了浮点数的表示,并编写了下面的代码。

该代码在 3 个单独的组中打印浮点数的二进制表示

SIGN EXPONENT FRACTION

然后它打印一个总和,当以足够的精度求和时,它将显示硬件中真正存在的值。

因此,当您编写 float x = 999... 时,编译器会将该数字转换为函数 xx 打印的位表示形式,以便函数 yy 打印的总和等于给定数字。

实际上,这个总和只是一个近似值。对于数字 999,999,999,编译器将在浮点数的位表示中插入数字 1,000,000,000

在代码之后,我附加了一个控制台会话,在该会话中,我计算了硬件中真正存在的两个常量(减去 PI 和 999999999)的项之和,由编译器插入其中。

#include <stdio.h>
#include <limits.h>

void
xx(float *x)
{
    unsigned char i = sizeof(*x)*CHAR_BIT-1;
    do {
        switch (i) {
        case 31:
             printf("sign:");
             break;
        case 30:
             printf("exponent:");
             break;
        case 23:
             printf("fraction:");
             break;

        }
        char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
        printf("%d ", b);
    } while (i--);
    printf("\n");
}

void
yy(float a)
{
    int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
    int fraction = ((1<<23)-1)&(*(int*)&a);
    int exponent = (255&((*(int*)&a)>>23))-127;

    printf(sign?"positive" " ( 1+":"negative" " ( 1+");
    unsigned int i = 1<<22;
    unsigned int j = 1;
    do {
        char b=(fraction&i)!=0;
        b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
    } while (j++, i>>=1);

    printf("*2^%d", exponent);
    printf("\n");
}

void
main()
{
    float x=-3.14;
    float y=999999999;
    printf("%lu\n", sizeof(x));
    xx(&x);
    xx(&y);
    yy(x);
    yy(y);
}

这是一个控制台会话,我在其中计算硬件中存在的浮点数的实际值。我使用 bc 打印主程序输出的项的总和。可以在 python repl 或类似的东西中插入该总和。

-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872

而已。 999999999的值其实是

999999999.999999446351872

您还可以使用 bc 检查 -3.14 也受到干扰。不要忘记在 bc 中设置一个 scale 因子。

显示的总和是硬件内部的总和。您通过计算获得的值取决于您设置的比例。我确实将 scale 因子设置为 15。在数学上,以无限的精度,它似乎是 1,000,000,000。


p
plugwash

浮点数的陷阱是它们看起来像十进制,但它们以二进制工作。

2 的唯一质因数是 2,而 10 的质因数是 2 和 5。结果是每个可以精确写为二进制分数的数字也可以精确写为十进制分数,但只有可以写成十进制分数的数字可以写成二进制分数。

浮点数本质上是具有有限有效数字的二进制分数。如果超过这些有效数字,则结果将四舍五入。

当您在代码中键入文字或调用函数将浮点数解析为字符串时,它需要一个十进制数,并将该十进制数的二进制近似值存储在变量中。

当您打印浮点数或调用函数以将其转换为字符串时,它会打印浮点数的十进制近似值。可以将二进制数精确地转换为十进制数,但在转换为字符串 * 时,我所知道的任何语言默认情况下都不会这样做。一些语言使用固定数量的有效数字,其他语言使用最短的字符串“往返”回到相同的浮点值。

Python 确实在将浮点数转换为“decimal.Decimal”时进行精确转换。这是我所知道的获得浮点数的精确十进制等值的最简单方法。


b
blackbrandt

Since Python 3.5 您可以使用 math.isclose() 函数来测试近似相等:

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False

T
Torsten Becker

另一种看待这个问题的方式:使用 64 位来表示数字。因此,无法精确表示超过 2**64 = 18,446,744,073,709,551,616 个不同的数字。

然而,Math 说 0 和 1 之间已经有无数个小数。IEE 754 定义了一种编码,可以有效地将这 64 位用于更大的数字空间加上 NaN 和 +/- Infinity,因此在精确表示的数字之间存在差距数字只是近似值。

不幸的是,0.3 存在差距。


Q
Quantum Sushi

浮点数在硬件级别表示为二进制数的分数(以 2 为基数)。例如小数部分:

0.125

具有值 1/10 + 2/100 + 5/1000 并且以同样的方式具有二进制分数:

0.001

值为 0/2 + 0/4 + 1/8。这两个分数具有相同的值,唯一的区别是第一个是十进制分数,第二个是二进制分数。

不幸的是,大多数十进制分数不能以二进制分数精确表示。因此,一般情况下,您给出的浮点数仅近似为要存储在机器中的二进制分数。

以 10 为底的问题更容易解决。例如,分数 1/3。您可以将其近似为小数:

0.3

或更好,

0.33

或更好,

0.333

等等。不管你写了多少个小数位,结果永远不会正好是 1/3,但它是一个总是更接近的估计值。

同样,无论您使用多少个以 2 为底的小数位,十进制值 0.1 都不能完全表示为二进制小数。在底数 2 中,1/10 是以下周期数:

0.0001100110011001100110011001100110011001100110011 ...

停在任何有限数量的位上,你会得到一个近似值。

对于 Python,在典型的机器上,浮点数的精度使用 53 位,因此输入十进制 0.1 时存储的值是二进制小数。

0.00011001100110011001100110011001100110011001100110011010

接近但不完全等于 1/10。

由于浮点数在解释器中的显示方式,很容易忘记存储的值是原始小数的近似值。 Python 仅显示以二进制形式存储的值的十进制近似值。如果 Python 要输出存储为 0.1 的二进制近似值的真实十进制值,它将输出:

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

这比大多数人预期的要多得多,因此 Python 显示一个四舍五入的值以提高可读性:

>>> 0.1
0.1

重要的是要理解这实际上是一种错觉:存储的值并不完全是 1/10,只是在显示屏上存储的值被四舍五入。一旦您使用这些值执行算术运算,这一点就会变得很明显:

>>> 0.1 + 0.2
0.30000000000000004

这种行为是机器浮点表示的本质所固有的:它不是 Python 中的错误,也不是代码中的错误。您可以在使用硬件支持计算浮点数的所有其他语言中观察到相同类型的行为(尽管某些语言默认情况下不会使差异可见,或者并非在所有显示模式下都可见)。

另一个惊喜是这个固有的。例如,如果您尝试将值 2.675 舍入到小数点后两位,您将得到

>>> round (2.675, 2)
2.67

round() 原语的文档表明它舍入到离零最近的值。由于小数部分正好在 2.67 和 2.68 之间,因此您应该期望得到(二进制近似值)2.68。然而,情况并非如此,因为当小数部分 2.675 转换为浮点数时,它以一个近似值存储,其精确值为:

2.67499999999999982236431605997495353221893310546875

由于近似值比 2.68 略接近 2.67,因此舍入向下。

如果您处于将十进制数字舍入一半很重要的情况,您应该使用十进制模块。顺便说一下,十进制模块还提供了一种方便的方法来“查看”为任何浮点数存储的确切值。

>>> from decimal import Decimal
>>> Decimal (2.675)
>>> Decimal ('2.67499999999999982236431605997495353221893310546875')

0.1 不完全存储在 1/10 中的另一个结果是 0.1 的十个值的总和也不等于 1.0:

>>> sum = 0.0
>>> for i in range (10):
... sum + = 0.1
...>>> sum
0.9999999999999999

二进制浮点数的算法有很多这样的惊喜。 “0.1”的问题在下面的“表示错误”部分中详细解释。有关此类意外的更完整列表,请参阅浮点的危险。

确实没有简单的答案,但是不要过分怀疑浮动虚拟数!在 Python 中,浮点数运算中的错误是由底层硬件引起的,并且在大多数机器上,每次操作的错误率不超过 2 ** 53。这对于大多数任务来说是非常必要的,但您应该记住,这些不是十进制运算,并且对浮点数的每个运算都可能会遇到新的错误。

尽管存在病态案例,但对于大多数常见用例,您只需将显示屏上的小数位数四舍五入即可获得预期结果。要更好地控制浮点数的显示方式,请参阅字符串格式语法以了解 str.format () 方法的格式规范。

这部分答案详细解释了“0.1”的例子,并展示了如何自己对这类案例进行精确分析。我们假设您熟悉浮点数的二进制表示。术语表示错误意味着大多数十进制分数不能精确地用二进制表示。这是 Python(或 Perl、C、C++、Java、Fortran 和许多其他)通常不以十进制显示确切结果的主要原因:

>>> 0.1 + 0.2
0.30000000000000004

为什么 ? 1/10 和 2/10 不能用二进制分数精确表示。但是,今天(2010 年 7 月)的所有机器都遵循 IEEE-754 浮点数算术标准。大多数平台使用“IEEE-754 双精度”来表示 Python 浮点数。双精度 IEEE-754 使用 53 位精度,因此在读取计算机时会尝试将 0.1 转换为 J / 2 ** N 形式的最接近的小数,其中 J 是正好 53 位的整数。重写:

1/10 ~ = J / (2 ** N)

在 :

J ~ = 2 ** N / 10

记住 J 正好是 53 位(所以> = 2 ** 52 但 <2 ** 53),N 的最佳可能值是 56:

>>> 2 ** 52
4503599627370496
>>> 2 ** 53
9007199254740992
>>> 2 ** 56/10
7205759403792793

所以 56 是 N 的唯一可能值,它正好为 J 留下 53 位。因此,J 的最佳可能值是这个商,四舍五入:

>>> q, r = divmod (2 ** 56, 10)
>>> r
6

由于进位大于 10 的一半,通过四舍五入获得最佳近似值:

>>> q + 1
7205759403792794

因此,“IEEE-754 双精度”中 1/10 的最佳近似值是高于 2 ** 56,即:

7205759403792794/72057594037927936

请注意,由于向上舍入,结果实际上略大于 1/10;如果我们没有四舍五入,商将略小于 1/10。但在任何情况下都不完全是 1/10!

所以计算机永远不会“看到”1/10:它看到的是上面给出的确切分数,使用“IEEE-754”中的双精度浮点数的最佳近似值:

>>>. 1 * 2 ** 56
7205759403792794.0

如果我们将这个分数乘以 10 ** 30,我们可以观察到它的小数点后 30 位的强权值。

>>> 7205759403792794 * 10 ** 30 // 2 ** 56
100000000000000005551115123125L

这意味着存储在计算机中的确切值大约等于十进制值 0.100000000000000005551115123125。在 Python 2.7 和 Python 3.1 之前的版本中,Python 将这些值四舍五入到 17 个有效小数位,显示“0.10000000000000001”。在当前版本的 Python 中,显示的值是分数尽可能短的值,但在转换回二进制时给出完全相同的表示,仅显示“0.1”。


D
Daniel McLaury

想象一下以 10 为基数工作,例如 8 位数的准确度。你检查是否

1/3 + 2 / 3 == 1

并知道这会返回 false。为什么?好吧,作为实数,我们有

1/3 = 0.333.... 和 2/3 = 0.666....

截断小数点后八位,我们得到

0.33333333 + 0.66666666 = 0.99999999

当然,它与 1.00000000 正好相差 0.00000001

具有固定位数的二进制数的情况完全类似。作为实数,我们有

1/10 = 0.0001100110011001100...(基数 2)

1/5 = 0.0011001100110011001...(基数 2)

如果我们将这些截断为七位,那么我们会得到

0.0001100 + 0.0011001 = 0.0100101

而另一方面,

3/10 = 0.01001100110011...(基数 2)

其中,被截断为 7 位,是 0.0100110,它们之间正好相差 0.0000001

确切的情况稍微微妙一些,因为这些数字通常以科学记数法存储。因此,例如,我们可以将其存储为 1.10011 * 2^-4 之类的东西,而不是将 1/10 存储为 0.0001100,这取决于我们为指数和尾数分配了多少位。这会影响您为计算获得的精度位数。

结果是由于这些舍入错误,您基本上不想在浮点数上使用 == 。相反,您可以检查它们的差值的绝对值是否小于某个固定的小数。


W
Wai Ha Lee

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


W
Wai Ha Lee

0.10.20.3 等十进制数在二进制编码的浮点类型中没有精确表示。 0.10.2 的近似值之和与 0.3 所用的近似值不同,因此 0.1 + 0.2 == 0.3 的错误可以在此处更清楚地看到:

#include <stdio.h>

int main() {
    printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
    printf("0.1 is %.23f\n", 0.1);
    printf("0.2 is %.23f\n", 0.2);
    printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
    printf("0.3 is %.23f\n", 0.3);
    printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
    return 0;
}

输出:

0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17

为了更可靠地评估这些计算,您需要对浮点值使用基于十进制的表示。默认情况下,C 标准不指定此类类型,而是作为 technical Report 中描述的扩展。

_Decimal32_Decimal64_Decimal128 类型可能在您的系统上可用(例如,GCCselected targets 上支持它们,但 ClangOS X 上不支持它们)。


P
Piedone

由于这个线程分支了一些关于当前浮点实现的一般性讨论,我想补充一点,有一些项目可以解决他们的问题。

例如,看一下 https://posithub.org/,它展示了一种称为 posit(及其前身 unum)的数字类型,它承诺以更少的位数提供更高的准确性。如果我的理解是正确的,它也解决了问题中的那种问题。相当有趣的项目,其背后的人是数学家它Dr. John Gustafson。整件事都是开源的,在 C/C++、Python、Julia 和 C# (https://hastlayer.com/arithmetics) 中有许多实际实现。


R
RollerSimmer

普通算术是以 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) ...