浮点数学运算是否损坏?


2978

考虑以下代码:

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

为什么会出现这些错误?


127
浮点变量通常具有这种行为。这是由它们在硬件中的存储方式引起的。有关更多信息,请查看有关浮点数Wikipedia文章
本S

62
JavaScript将小数视为浮点数,这意味着加法之类的操作可能会舍入错误。您可能想看一下这篇文章:每位计算机科学家都应该了解浮点算法
matt b

4
仅供参考,javascript中的所有数字类型均为IEEE-754 Double。
加里·威洛比

6
因为JavaScript对数学使用IEEE 754标准,所以它使用64位浮点数。简而言之,这会导致精度错误,这是由于计算机工作在Base 2中,而十进制是Base 10时,这会导致精度错误。
Pardeep Jain

Answers:


2245

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

对于0.1标准binary64格式,表示形式可以完全按照

相比之下,合理数量0.1,这是1/10可以完全按照书面

  • 0.1 以十进制表示,或
  • 0x1.99999999999999...p-4以C99十六进制表示法的类似形式表示,其中...表示9的无休止序列。

常量0.20.3程序中的常量也将接近其真实值。碰巧的是,最近double0.2低于合理数量较大0.2但最近double0.3低于合理数量较小0.3。的总和0.10.2卷起比有理数较大0.3,并因此与在代码中不同意恒定。

每个计算机科学家都应该了解浮点算术,是对浮点算术问题的相当全面的处理。有关更容易理解的说明,请参见floating-point-gui.de

旁注:所有位置(以N为底的)数字系统均会精确地共享此问题

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

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

**当然,这并不完全是将浮点数存储在内存中的方式(它们使用科学计数形式)。但是,它的确说明了二进制浮点精度误差趋于增加的观点,因为我们通常感兴趣的“真实世界”数通常是10的幂-但这仅仅是因为我们使用了十进制数天-今天。这也是为什么我们要说71%而不是“每7个中的5个”(71%是一个近似值,因为5/7不能用任何十进制数字精确表示)的原因。

否:二进制浮点数没有被破坏,它们恰好与其他所有基数N的系统一样不完美:)

侧面说明:在编程中使用浮点数

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

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

千万不能if (x == y) { ... }

反而做if (abs(x - y) < myToleranceValue) { ... }

abs绝对值在哪里。myToleranceValue需要为您的特定应用选择-这与您准备允许多少“摆动空间”以及要比较的最大数字有很大关系(由于精度问题) )。当心所选语言中的“ epsilon”样式常量。这些不得用作公差值。


181
我认为“某些错误常量”比“ Epsilon”更正确,因为没有“ Epsilon”可以在所有情况下使用。在不同情况下需要使用不同的ε。机器epsilon几乎永远不是一个好常数。
Rotsor

34
这不是不错,所有的浮点运算是基于IEEE [754]标准。例如,仍然有一些使用旧的IBM十六进制FP的系统,并且仍然有不支持IEEE-754算术的图形卡。但是,以合理的近似是正确的。
斯蒂芬·佳能

19
Cray放弃了IEEE-754的速度要求。Java也放松了对优化的坚持。
艺术泰勒

28
我认为您应该在此答案中添加一些内容,以说明如何始终,始终使用定点算术对整数进行货币计算,因为货币是量化的。(在内部会计计算中,以很小的几分之一来计算,或者无论您使用的最小货币单位是多少,这都很有用-这通常有助于例如在将“每月29.99美元”转换为每日汇率时减少舍入误差-但它应该仍然是定点算术。)
zwol

18
有趣的事实:这个0.1不能准确地用二进制浮点表示,导致臭名昭著的爱国者导弹软件漏洞,导致第一次伊拉克战争期间有28人丧生。
hdl

602

硬件设计师的观点

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

1.概述

从工程的角度来看,大多数浮点运算都将具有一定的错误元素,因为进行浮点计算的硬件仅要求最后的误差小于一个单元的一半。因此,很多硬件将停止在这样的精度上,该精度仅对于单次操作在最后一次产生的误差小于一个单元的一半是必要的,这在浮点除法中尤其成问题。构成单个操作的要素取决于该单元采用的操作数。对于大多数情况,它是两个,但是某些单位使用3个或更多操作数。因此,不能保证重复操作会导致理想的错误,因为随着时间的推移这些错误会累加。

2.标准

大多数处理器遵循IEEE-754标准,但有些处理器使用非规范化或不同的标准。例如,IEEE-754中存在一种非规范化模式,该模式允许以精度为代价表示非常小的浮点数。但是,以下内容将涵盖IEEE-754的标准化模式,这是典型的操作模式。

在IEEE-754标准中,只要最后一位小于一个单位的一半,并且允许硬件设计者允许任何误差/ε值,并且最后的结果必须小于一个单位的一半。一个手术的地方。这解释了为什么当重复操作时,错误加起来。对于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。因此,例如,用于一次计算2位商(基数4)的除法器的典型商选择表就是2+2= 4位(加上一些可选位)。

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

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

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

所有操作中舍入错误的另一个原因是IEEE-754允许的最终答案截断的不同模式。有截断,四舍五入,四舍五入(默认值),四舍五入和四舍五入。对于单个操作,所有方法最后都会引入误差小于1个单位的元素。随着时间的流逝和重复的操作,截断还会累积地导致错误。截断误差在取幂时尤其成问题,涉及某种形式的重复乘法。

5.重复操作

由于执行浮点计算的硬件只需要产生一个结果,该结果的单个操作的最后一个位置的误差小于一个单元的一半,因此如果不注意,该误差将随着重复的操作而扩大。这就是为什么在需要有限误差的计算中,数学家会使用诸如 IEEE-754 的最后一位使用四舍五入到最接近的偶数之类的方法的原因,因为随着时间的流逝,误差更可能相互抵消。和间隔算术结合IEEE 754舍入模式的变体预测舍入误差并进行更正。由于与其他舍入模式相比其相对误差较低,因此舍入到最接近的偶数位(在最后一位)是IEEE-754的默认舍入模式。

请注意,默认的舍入模式(最后一位舍入到最接近的偶数位)保证一次操作的最后一位的误差小于一个单位的一半。单独使用截断,舍入和舍入可能会导致错误,该错误大于最后一个单元的一半,但小于最后一个单元,因此不建议使用这些模式,除非它们是在间隔算术中使用。

6.总结

简而言之,浮点运算错误的根本原因是硬件的截断和除法时的倒数截断的组合。由于IEEE-754标准在一次操作中只要求最后一个位置的误差小于一个单元的一半,因此,除非进行纠正,否则重复操作中的浮点错误将加起来。


8
(3)是错误的。在一个部门的舍入误差不小于一个在过去的所在单位,但最多一半在最后一位的单位。
gnasher729 2014年

6
@ gnasher729好收获。使用默认IEEE舍入模式时,大多数基本操作的en误差最后最后还不到一个单位的1/2。编辑了说明,还注意到,如果用户覆盖默认的舍入模式,则错误可能大于1 ulp的1/2但小于1 ulp(在嵌入式系统中尤其如此)。
KernelPanik

39
(1)浮点数字没有错误。每个浮点值就是它的确切值。大多数(但不是全部)浮点运算给出的结果都不准确。例如,没有二进制浮点值正好等于1.0 / 10.0。另一方面,某些操作(例如1.0 + 1.0)确实给出了准确的结果。
所罗门慢

19
“浮点除法错误的主要原因是用于计算商的除法算法”,这是一个非常令人误解的说法。对于符合IEEE-754的除法,导致浮点除法的唯一错误原因是无法以结果格式准确表示结果。无论使用哪种算法,都将计算出相同的结果。
斯蒂芬·佳能

6
@Matt对不起,您的回复很晚。这基本上是由于资源/时间问题和权衡所致。有一种方法可以进行长除法/更“正常”的除法,即所谓的SRT除法,其基数为2。但是,这会反复移位并从除数中减去除数,并花费许多时钟周期,因为它每个时钟周期仅计算一位的商。我们使用倒数表,以便我们可以计算每个周期更多的商位,并进行有效的性能/速度折衷。
KernelPanik '16

462

当您将.1或1/10转换为以2为基数(二进制)时,您会在小数点后得到一个重复模式,就像试图以10为基数表示1/3。该值不准确,因此您无法执行使用普通的浮点方法进行精确数学运算。


133
简短的回答。重复模式看起来像0.0001100110011001100110011001100110011001100110011001111 ...
Konstantin Chernov

4
这不能解释为什么没有使用更好的算法而不首先转换为二进制文件。
德米特里·扎伊采夫

12
因为表现。使用二进制文件的速度快了数千倍,因为它是计算机固有的。
Joel Coehoorn

7
有一些产生精确十进制值的方法。BCD(二进制编码的十进制)或其他各种形式的十进制数字。但是,与使用二进制浮点运算相比,它们既较慢(很多速度较慢),而且占用的存储空间也更多。(例如,打包的BCD在一个字节中存储2个十进制数字。这是一个字节中的100个可能的值,实际上可以存储256个可能的值或100/256,这浪费了大约60%的字节的可能值。)
Duncan C

16
@Jacksonkr,您还在以10为基数进行思考。计算机以2为基数。
乔尔·科洪

306

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

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

比萨切刀的动作非常精细,如果您从整个比萨开始,则将其减半,然后每次将最小的薄片减半,则可以将薄片减半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.29999999999999998888883459576368331909179575。


PS一些编程语言还提供了比萨饼切割器,可以将切成精确的十分之一。尽管这种比萨饼切割器并不常见,但是如果您确实有机会使用它,那么在重要的是能够精确获得十分之一或五分之一的切片时,应该使用它。

(最初发布在Quora上。)


3
请注意,有些语言包含精确的数学运算。一个例子是Scheme,例如通过GNU Guile。请参阅draketo.de/english/exact-math-to-the-rescue-这些将数学保留为分数,并且仅在最后进行切分。
Arne Babenhauserheide 2014年

5
@FloatingRock实际上,很少有主流编程语言内置有理数。就像我一样,阿恩(Arne)是一名策划者,所以这些都是我们宠爱的东西。
克里斯·杰斯特·杨

5
@ArneBabenhauserheide我认为值得补充的是,这仅适用于有理数。因此,如果您使用pi等非理性数字进行数学运算,则必须将其存储为pi的倍数。当然,任何涉及pi的计算都不能表示为精确的十进制数。
Aidiakapi 2015年

13
@connexo好的。您将如何对披萨旋转器进行编程以使其达到36度?什么是36度?(提示:如果您能够以精确的方式定义它,那么您还可以拥有十分精确的切片比萨饼切刀。)换句话说,您实际上不能拥有1/360(度)或1 / 10(36度),只有二进制浮点数。
克里斯·杰斯特·杨

12
@connexo另外,“每个白痴”都不能将比萨饼精确地旋转36度。人类太容易出错,无法做任何如此精确的事情。
克里斯·杰斯特·杨

212

浮点舍入错误。由于缺少素数5,所以0.1在base-2中不能像在base-10中那样精确地表示。就像1/3可以用无数个数字来表示十进制一样,而在base-3中则是“ 0.1”, 0.1在base-2中采用无数位数,而在base-10中则采用无数位数。而且计算机没有无限的内存量。


133
计算机不需要无限数量的内存即可获得0.1 + 0.2 = 0.3对
Pacerier 2011年

23
@Pacerier当然,他们可以使用两个无界精度整数表示分数,或者可以使用引号表示法。正是这种“二进制”或“十进制”的概念使这一点变得不可能-想法是您拥有一个二进制/十进制数字序列以及一个小数点。为了获得精确的合理结果,我们需要更好的格式。
Devin Jeanpierre 2011年

15
@Pacerier:二进制或十进制浮点都不能精确存储1/3或1/13。十进制浮点类型可以精确地表示形式为M / 10 ^ E的值,但是在表示大多数其他分数时,精度不如大小相似的二进制浮点数。在许多应用中,使用任意分数具有更高的精度比使用一些“特殊”精度具有完美的精度更为有用。
supercat 2014年

13
@Pacerier 如果将数字存储为二进制浮点数,它们就会这样做,这就是答案所在。
Mark Amery 2014年

3
@chux:二进制和十进制类型之间的精度差异并不大,但是十进制类型在最佳情况下与最坏情况下的10:1差异远大于二进制类型在2:1上的差异。我很好奇是否有人已经构建硬件或编写软件来有效地对两种十进制类型进行操作,因为这似乎都不适合在硬件或软件中进行有效的实现。
2015年

121

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

例如:

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页)


3
问题在于转换本身是不准确的。16.08 * 100 = 1607.9999999999998。我们是否必须求助于拆分数字并分别进行转换(如16 * 100 + 08 = 1608)?
杰森

38
这里的解决方案是用整数进行所有计算,然后除以您的比例(在这种情况下为100),仅在显示数据时才取整。这样可以确保您的计算始终准确。
David Granado

15
只是略微提一下:整数算术仅在浮点到一个点(双关目标)上才是精确的。如果数字大于0x1p53(使用Java 7的十六进制浮点表示法= 9007199254740992),则ulp在该点为2,因此0x1p53 + 1向下舍入为0x1p53(0x1p53 + 3则四舍五入为0x1p53 + 4,由于四舍五入)。:-D但是可以肯定的是,如果您的数字小于9千万,则应该没问题。:-P
克里斯·杰斯特·杨

2
杰森,您应该将结果取整(int)(16.08 * 100 + 0.5)
米哈伊尔·塞梅诺夫

@CodyBugstein“ 那么,如何显示.1 + .2以显示.3? ”编写自定义打印函数,将小数点放在所需的位置。
罗恩·约翰

113

我的回答很长,因此我将其分为三个部分。由于问题是关于浮点数学的,因此我将重点放在机器的实际功能上。我还专门针对双精度(64位)精度,但是该参数同样适用于任何浮点运算。

前言

一个IEEE 754双精度二进制浮点格式(binary64)数表示数字形式的

值=(-1)^ s *(1.m 51 m 50 ... m 2 m 1 m 02 * 2 e-1023

64位:

  • 第一位是符号位1如果数字为负数,0否则为1
  • 接下来的11位是指数,它偏移 1023。换句话说,从双精度数读取指数位后,必须减去1023以获得2的幂。
  • 剩余的52位为有效数字(或尾数)。在尾数中,由于所有二进制值的最高有效位1.是,所以总是省略2的“隐含” 1

1 -IEEE 754允许使用带符号零的概念- +0-0以不同的方式对待:1 / (+0)正无穷大;1 / (-0)是负无穷大。对于零值,尾数和指数位均为零。注意:零值(+0和-0)没有明确地归类为非正规2

2- 非正规数不是这种情况,非正规数的偏移指数为零(并且是隐含的0.)。反规范双精度数的范围是d 分钟 ≤| X | ≤d 最大,其中d 分钟(最小可表示非零数)为2 -1023 - 51(≈4.94 * 10 -324)和d 最大值(最大的反规范数,其尾数完全由1s)为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);
}

切入点:原始问题

(跳到底部的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 * 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

由于总和不是2 n * 1. {bbb} 的形式,因此我们将指数增加1并将小数点(二进制)移至:

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

现在尾数中有53位(第53位在上一行的方括号中)。IEEE 754 的默认舍入模式为“ 最接近舍入 ”-即,如果数字x介于两个值ab之间,则选择最低有效位为零的值。

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; DR

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:最后几位是四舍五入的。


14
我的回答在发布后不久被否决。从那以后,我进行了许多更改(包括以二进制形式写入0.1和0.2时明确地指出了重复的位,在原始示例中已将其省略)。如果下选民看到了这个偶然的机会,请您给我一些反馈,以便我改善自己的答案?我觉得我的答案增加了一些新内容,因为在其他答案中没有以相同的方式涵盖IEEE 754中对和的处理。尽管“每位计算机科学家应该知道的...”涵盖了相同的内容,但我的回答专门针对 0.1 + 0.2的情况。
李慧夏

57

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

如果计算机以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.5is 1 x 2⁻¹0.25is 1 x 2⁻²,并将它们的结果加到3 x 2⁻²或中0.75。究竟。

问题在于数字可以精确地以10为底,而不能以2为底。这些数字需要四舍五入到最接近的等值。假设使用非常常见的IEEE 64位浮点格式,则最接近的数字0.13602879701896397 x 2⁻⁵⁵,最接近的数字0.27205759403792794 x 2⁻⁵⁵;将它们相加会得出10808639105689191 x 2⁻⁵⁵或的精确十进制值0.3000000000000000444089209850062616169452667236328125。浮点数通常会四舍五入以显示。


2
@Mark谢谢您的明确解释,但是随后出现了一个问题,为什么0.1 + 0.4精确地相加为0.5(至少在Python 3中)。另外,在Python 3中使用浮点数时检查相等性的最佳方法是什么?
pchegoor

2
@ user2417881 IEEE浮点运算的每个运算都有取整规则,有时即使两个数字相差很小,取整也可以产生准确的答案。详细信息太长,无法发表评论,无论如何我都不是专家。正如您在此答案中看到的那样,0.5是可以用二进制表示的少数十进制之一,但这只是一个巧合。有关平等性测试,请参见stackoverflow.com/questions/5595425/…
Mark Ransom '18

1
@ user2417881您的问题引起了我的兴趣,因此我将其变成一个完整的问题和答案:stackoverflow.com/q/48374522/5987
Mark Ransom

47

浮点舍入错误。从每位计算机科学家应该了解的浮点算法中

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


33

我的解决方法:

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

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


30

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

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

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

在此处输入图片说明

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

因此,要准确地表示这些数字有些困难。即使您明确指定此变量,也无需任何中间计算。


28

一些统计数据与此著名的双精度问题有关。

当以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。如果使用两位小数(第0.01步),情况会进一步恶化(18%和36%)。


28

否,不折断,但大多数十进制小数必须近似

摘要

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

即使简单的数字(如0.01、0.02、0.03、0.04 ... 0.24)也不能精确表示为二进制分数。如果您将0.01,.02,.03 ...相加,则直到达到0.25时,您才能获得以2为底的可表示的第一个分数。如果您尝试使用FP,则0.01的值可能会略有偏差,因此将25个值相加达到精确的0.25的唯一方法是需要一长串因果关系,包括保护位和舍入。很难预测,所以我们举起手来说“ FP是不精确的”,但这不是真的。

我们不断地给FP硬件一些东西,这些东西在base 10中看起来很简单,但在base 2中却是重复的部分。

这怎么发生的?

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

           一个/(2 n x 5 m

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

           a / 2 n

因此,在小数,我们不能代表1 / 3。因为以10为底的素数包括2作为素数,所以我们可以写为二进制分数的每个数字可以被写为以10为底的分数。但是,几乎所有我们以10为基数的分数都无法用二进制表示。在0.01、0.02、0.03 ... 0.99的范围内,我们的FP格式只能表示三个数字:0.25、0.50和0.75,因为它们都是1 / 4、1 / 2和3/4,所有数字仅使用2 n项的素数因子。

在基座10,我们不能代表1 / 3。但是,在二进制,我们不能做1 / 10 1 / 3

因此,虽然每个二进制分数都可以用十进制表示,但事实并非如此。实际上,大多数十进制小数都以二进制重复。

处理它

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

同样,在实数运算问题(FP是在早期的,价格昂贵的早期计算机上发明的问题)上,宇宙的物理常数和所有其他度量值仅由相对较少的有效数字知道,因此整个问题空间无论如何都是“ inexact”。FP“准确性”在这种应用程序中不是问题。

当人们尝试使用FP进行豆计数时,确实会出现整个问题。它确实可以做到这一点,但前提是您坚持使用整数值,这会破坏使用它的意义。这就是为什么我们拥有所有这些十进制分数软件库的原因。

我喜欢Chris的Pizza回答,因为它描述了实际的问题,而不仅仅是描述“不准确性”的惯常做法。如果FP只是“不准确”,我们可以解决这个问题,几十年前就可以做到。我们之所以没有这样做,是因为FP格式紧凑,快速,并且是处理大量数字的最佳方法。而且,这是航天时代和军备竞赛的遗留下来的,也是早期尝试解决使用小型内存系统的非常慢的计算机来解决大问题的尝试。(有时,单个磁芯用于1位存储,但这是另一回事了。

结论

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


在所有情况下,四舍五入到最接近的整数都不是解决比较问题的安全方法。0.4999998和0.500001舍入为不同的整数,因此每个舍入切点周围都有一个“危险区域”。(我知道这些十进制字符串可能不能完全表示为IEEE二进制浮点数。)
Peter Cordes

1
另外,即使浮点是“旧版”格式,它的设计也很好。我不知道如果现在重新设计,任何人都会改变。我越了解它,越觉得它真的很好设计的。例如,有偏指数表示连续的二进制浮点数具有连续的整数表示形式,因此您可以nextafter()对IEEE浮点数的二进制表示形式进行整数递增或递减实现。另外,您可以将浮点数作为整数进行比较,并获得正确的答案,除非它们均为负数(因为符号幅度与2的补码比较)。
彼得·科德斯

我不同意,浮点数应存储为小数而不是二进制,并且所有问题都可以解决。
罗南·费斯汀格

x /(2 ^ n + 5 ^ n) ”不应该是“ x /(2 ^ n * 5 ^ n) ”吗?
李慧夏

@RonenFestinger-1/3呢?
斯蒂芬·C

19

您尝试过胶带解决方案吗?

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

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

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


19

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

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

让我解释一下为什么这是最好的解决方案。正如上面提到的其他答案一样,最好使用可立即使用的Javascript toFixed()函数来解决问题。但是您很可能会遇到一些问题。

想象一下,你要添加两个浮点数喜欢0.20.7这里是:0.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之后的那些不需要的零?现在是时候将其转换为浮动以使其如您所愿了:

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


这让我非常头疼。我求和了12个浮点数,然后显示求和数和平均值。使用toFixed()可能会固定两个数字的总和,但是当多个数字求和时,飞跃就很明显。
Nuryagdy Mustapayev

@Nuryagdy Mustapayev我没有得到您的意图,因为在您可以求和12个浮点数之前,我进行了测试,然后对结果使用floatify()函数,然后对它执行任何操作,我发现使用它没有问题。
Mohammad Musavi

我只是说在我有大约20个参数和20个公式的情况下,每个公式的结果都取决于其他公式,这种解决方案无济于事。
Nuryagdy Mustapayev

16

出现这些怪异的数字是因为计算机出于计算目的使用二进制(基数2)数字系统,而我们使用十进制(基数10)。

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


我完全不理解你的第二段。
Nae

1
@Nae我将第二段翻译为“大多数分数不能精确地用十进制二进制表示。因此,大多数结果将四舍五入-尽管它们仍然精确到表示中固有的位数/位数正在使用。”
史蒂夫·萨米特

15

这个问题的许多重复项中有许多都询问浮点取整对特定数字的影响。在实践中,通过查看感兴趣的计算的确切结果,而不是仅仅阅读它,会更容易感觉到它是如何工作的。有些语言提供了这样做的方式-如转换floatdoubleBigDecimalJava编写的。

由于这是与语言无关的问题,因此需要与语言无关的工具,例如小数到浮点转换器

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

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

......此外,轮连二进制表示,没有十进制表示。看到或例如this
Wai Ha Lee

@WaiHaLee我没有对任何十进制数字(只有十六进制)应用奇/偶检验。十六进制数字即使且仅当其二进制扩展的最低有效位为零时才是。
Patricia Shanahan

14

鉴于没有人提及此事...

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

  • Python的decimal模块和Java的BigDecimalclass,内部用十进制表示法表示数字(与二进制表示法相对)。两者的精度都有限,因此它们仍然容易出错,但是它们使用二进制浮点算法解决了最常见的问题。

    处理货币时,小数非常好:十美分加二十美分总是正好是三十美分:

    >>> 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的BigFractionclass。两者都将有理数表示为(numerator, denominator)对,并且它们可能比十进制浮点算术给出更准确的结果。

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


14

我可以补充吗?人们总是认为这是计算机问题,但是如果您用手计数(以10为基数),(1/3+1/3=2/3)=true除非您将0.333 ...无限大地加到0.333 ...,否则您将无法获得结果,就像(1/10+2/10)!==3/10基数中的问题一样2,将其截断为0.333 + 0.333 = 0.666,可能会将其舍入为0.667,这在技术上也是不准确的。

以三进制数计算,三分之二不是问题-也许有些比赛每只手用15根​​手指会问为什么十进制数学运算被破坏了...


由于人类使用的是十进制数字,因此我看不出为什么没有默认将浮点数表示为十进制的充分理由,因此我们得出了准确的结果。
罗南·费斯廷格

人类使用除10进制(十进制)以外的许多基准,二进制是我们最常用于计算的基数。“充分的理由”是您无法简单地表示每个基数的每个分数

@RonenFestinger二进制算术易于在计算机上实现,因为它只需要八位带数字的基本运算:例如$ a $,$ b $ in $ 0,1 $,您只需要知道$ \ operatorname {xor}(a,b) $和$ \ operatorname {cb}(a,b)$,其中xor是异或,cb是“进位”,在所有情况下均为$ 0 $,但当$ a = 1 = b $时,在这种情况下,一个(实际上所有操作的可交换性为您节省了$ 2 $的情况,而您所需要的只是$ 6 $规则)。十进制扩展需要存储$ 10 \乘以11 $(十进制符号)的情况,每个位需要$ 10 $不同的状态,这会浪费进位存储空间。
Oskar Limka '18

@RonenFestinger-十进制不是更准确。这就是这个答案的意思。对于您选择的任何基数,都会有有理数(分数)给出无限重复的数字序列。作为记录,一些第一台计算机的确使用了以10为基数的表示形式,但是具有开创性的计算机硬件设计人员很快得出结论,以2为基础实现起来要容易得多,效率也更高。
斯蒂芬C,

9

可以在数字计算机中实现的那种浮点数学运算必须使用实数的近似值及其上的运算。(标准版本运行多达五十页的文档,并设有一个委员会来处理其勘误表并作进一步完善。)

这种近似是不同类型的近似的混合,由于其与精确度的特定偏离方式,每种近似都可以忽略不计或仔细考虑。它还在硬件和软件级别上涉及到许多明显的例外情况,大多数人会假装不注意而已。

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

但是,如果您对有时浮点数学的值模糊并且逻辑和错误会迅速累积的想法感到满意,并且可以编写要求和测试以允许这样做,那么您的代码就可以经常使用其中的内容您的FPU。


9

只是为了好玩,按照标准C99的定义,我玩了float的表示形式,并编写了以下代码。

该代码在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);
}

这是一个控制台会话,在其中计算硬件中存在的float的实际值。我曾经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是否也受到干扰。不要忘记在中设置一个scale因子bc

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


5

看待这种情况的另一种方式:使用64位来表示数字。因此,最多可以精确表示2 ** 64 = 18,446,744,073,709,551,616个不同的数字。

但是,Math说,在0和1之间已经存在无数个小数。IEE754定义了一种编码,可以有效地使用这64位来处理更大的数字空间,以及NaN和+/- Infinity,因此用数字填充的准确表示的数字之间存在间隙仅近似数字。

不幸的是0.3处于差距。


4

想象一下,以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为基)

如果我们将它们截断为7位,那么我们将得到

0.0001100 + 0.0011001 = 0.0100101

另一方面,

3/10 = 0.01001100110011 ...(以2为底)

被截断为7位,是0.0100110,并且它们之间的差异完全相同0.0000001


确切的情况稍微有些微妙,因为这些数字通常以科学计数法存储。因此,例如,根据我们为指数和尾数分配的位数,0.0001100我们可以像存储一样存储1/10,而不是像存储1/10一样1.10011 * 2^-4。这会影响您为计算获得的精度位数。

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


4

从Python 3.5开始,您可以使用math.isclose()函数测试近似相等:

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

3

由于该线程在当前浮点实现的一般讨论中有所分支,因此我补充说,有一些项目正在解决其问题。

https://posithub.org/为例,它展示了一个称为posit(及其前身unum)的数字类型,该数字类型有望以更少的位数提供更高的准确性。如果我的理解是正确的,那么它也可以解决问题中的这类问题。这个项目非常有趣,其背后的人物是约翰·古斯塔夫森博士(John Gustafson)的数学家。整个过程都是开源的,在C / C ++,Python,Julia和C#(https://hastlayer.com/arithmetics)中有许多实际实现。


3

实际上很简单。当您拥有以10为底的系统(如我们的系统)时,它只能表示使用底的素数的分数。10的素数是2和5。因此,由于分母都使用10的素数,所以1 / 2、1 / 4、1 / 5、1 / 8和1/10都可以清楚地表达。 / 3、1 / 6和1/7都是重复的小数,因为它们的分母使用3或7的质数。在二进制(或基数2)中,唯一的质数是2。因此,您只能清楚地表达小数仅包含2作为主要因子。以二进制形式,1 / 2、1 / 4、1 / 8都将干净地表示为小数。而1/​​5或1/10将重复小数。因此,在以10为基数的系统中使用干净的小数时,0.1和0.2(1/10和1/5)在计算机正在运行的以2为基数的系统中重复小数。对这些重复的小数进行数学运算时,

来自https://0.30000000000000004.com/


3

十进制数如0.10.2,和0.3不准确表示以二进制编码的浮点类型。和的近似值之0.10.2与用于的近似值不同0.3,因此在0.1 + 0.2 == 0.3这里可以更清楚地看到as 的虚假性:

#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标准默认不指定此类类型,而是作为技术报告中描述的扩展。

_Decimal32_Decimal64_Decimal128类型可能是您的系统上(例如,GCC支持他们选定的目标,但不支持他们在OS X)。


1

Math.sum(javascript)....一种替换运算符

.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001

Object.defineProperties(Math, {
    sign: {
        value: function (x) {
            return x ? x < 0 ? -1 : 1 : 0;
            }
        },
    precision: {
        value: function (value, precision, type) {
            var v = parseFloat(value), 
                p = Math.max(precision, 0) || 0, 
                t = type || 'round';
            return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
        }
    },
    scientific_to_num: {  // this is from https://gist.github.com/jiggzson
        value: function (num) {
            //if the number is in scientific notation remove it
            if (/e/i.test(num)) {
                var zero = '0',
                        parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
                        e = parts.pop(), //store the exponential part
                        l = Math.abs(e), //get the number of zeros
                        sign = e / l,
                        coeff_array = parts[0].split('.');
                if (sign === -1) {
                    num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
                } else {
                    var dec = coeff_array[1];
                    if (dec)
                        l = l - dec.length;
                    num = coeff_array.join('') + new Array(l + 1).join(zero);
                }
            }
            return num;
         }
     }
    get_precision: {
        value: function (number) {
            var arr = Math.scientific_to_num((number + "")).split(".");
            return arr[1] ? arr[1].length : 0;
        }
    },
    sum: {
        value: function () {
            var prec = 0, sum = 0;
            for (var i = 0; i < arguments.length; i++) {
                prec = this.max(prec, this.get_precision(arguments[i]));
                sum += +arguments[i]; // force float to convert strings to number
            }
            return Math.precision(sum, prec);
        }
    }
});

这个想法是使用数学运算符来避免浮点错误

Math.sum自动检测使用的精度

Math.sum接受任意数量的参数


1
我不确定您是否回答了“ 为什么会发生这些错误? ”这个问题。
惠哈利

以某种方式您是对的,但是我来自一个关于此问题的javascript奇怪行为……我只想分享一种解决方案
bortunac

0

我刚刚看到了围绕浮点数的这个有趣的问题:

考虑以下结果:

error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))
1

我们可以清楚地看到一个断点,当2**53+1-一切正常直到2**53

>>> (2**53) - int(float(2**53))
0

在此处输入图片说明

发生这种情况的原因是双精度二进制:IEEE 754双精度二进制浮点格式:binary64

从Wikipedia页面获取双精度浮点格式

双精度二进制浮点是PC上常用的格式,尽管其性能和带宽成本较高,但其范围比单精度浮点更宽。与单精度浮点格式一样,与相同大小的整数格式相比,它在整数上缺乏精度。通常简称为double。IEEE 754标准将binary64指定为具有:

  • 符号位:1位
  • 指数:11位
  • 精确度:53位(显式存储52位)

在此处输入图片说明

给定的64位双精度基准,给定的偏置指数和52位小数所假定的实数值为

在此处输入图片说明

要么

在此处输入图片说明

感谢@a_guest向我指出这一点。


-1

一个不同的问题已被命名为此重复项:

在C ++中,为什么结果与cout << x调试器显示的值不同x

x在的问题是一个float变量。

一个例子是

float x = 9.9F;

调试器显示9.89999962cout操作的输出为9.9

答案是,cout默认精度为float6,因此将其舍入为6个十进制数字。

请参阅此处以供参考


1
IMO-在此处发布此消息是错误的方法。我知道这很令人沮丧,但是需要原始问题答案的人(显然现在已删除!)在这里找不到。如果您确实认为自己的工作值得保存,我建议:1)寻找另一个可以回答的问题,2)创建一个自我回答的问题。
斯蒂芬C,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.