题 浮点数学是否破碎?


请考虑以下代码:

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

为什么会出现这些不准确之处?


2268
2018-02-25 21:39


起源


浮点变量通常具有此行为。这是由它们如何存储在硬件中引起的。有关更多信息,请查看 关于浮点数的维基百科文章。 - Ben S
JavaScript将小数视为 浮点数字,这意味着添加等操作可能会出现舍入错误。您可能想看看这篇文章: 每个计算机科学家应该知道的浮点运算 - matt b
仅供参考,javascript中的所有数字类型均为IEEE-754双打。 - Gary Willoughby
@Gary True,虽然您可以保证对于最多15位的整数具有完美的整数精度,请参阅 hunlock.com/blogs/The_Complete_Javascript_Number_Reference - Ender
因为JavaScript使用IEEE 754标准的数学,它使用 64位 浮动数字。简而言之,这会导致计算机工作时出现浮点(十进制)计算时的精度错误 基地2 小数是 基地10。 - Pardeep Jain


答案:


二进制 浮点 数学是这样的。在大多数编程语言中,它基于 IEEE 754标准。 JavaScript使用64位浮点表示,与Java相同 double。问题的关键在于数字以这种格式表示为2的幂的整数倍;有理数(如 0.1,是的 1/10)其分母不是2的幂,不能准确表示。

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

  • 0.1000000000000000055511151231257827021181583404541015625 十进制,或
  • 0x1.999999999999ap-4 在 C99 hexfloat表示法

相比之下,理性数字 0.1,是的 1/10,可以写成完全相同的

  • 0.1 十进制,或
  • 0x1.99999999999999...p-4 在C99 hexfloat符号的类比,其中 ... 代表9个无休止的序列。

常数 0.2 和 0.3 在你的程序中也将近似于它们的真实值。碰巧是最接近的 double 至 0.2 大于有理数 0.2 但那是最接近的 double 至 0.3 小于有理数 0.3。总数是 0.1 和 0.2 结束大于有理数 0.3 因此不同意代码中的常量。

对浮点算术问题的一个相当全面的处理是 每个计算机科学家应该知道的浮点运算。有关更容易理解的解释,请参阅 floating-point-gui.de


1723
2018-04-18 11:52



'一些误差常数'也称为Epsilon值。 - Gary Willoughby
我认为“一些误差常数”比“Epsilon”更正确,因为没有“Epsilon”可以在所有情况下使用。不同的epsilons需要在不同的情况下使用。机器epsilon几乎不是一个很好的常量。 - Rotsor
不是 相当 确实,所有浮点数学都基于IEEE [754]标准。例如,仍有一些系统正在使用旧的IBM十六进制FP,并且仍然存在不支持IEEE-754算法的图形卡。然而,合理的近似是正确的。 - Stephen Canon
Cray放弃了IEEE-754的速度合规性。 Java也放松了它作为优化的依从性。 - Art Taylor
我认为你应该在这个答案中添加一些关于钱应该如何计算的答案,总是用定点算法来完成 整数,因为钱是量化的。 (以一分钱的微小部分进行内部会计计算或任何最小的货币单位可能是有意义的 - 这通常有助于减少将“每月29.99美元”转换为每日费率时的四舍五入错误 - 但它应该仍然是定点算术。) - zwol


硬件设计师的观点

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

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。因此,例如,一次计算商的2位(基数4)的分频器的典型商数选择表将是 2+2= 4 位(加上几个可选位)。

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

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

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

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

5.重复操作

由于执行浮点计算的硬件仅需要产生一个结果,错误小于单个操作的最后一个单位的一半,如果没有观察,错误将在重复操作上增加。这就是在需要有界误差的计算中,数学家使用诸如使用舍入到最近的方法的原因 在最后一个地方甚至数字 IEEE-754,因为随着时间的推移,错误更容易相互抵消,并且 区间运算 结合的变化 IEEE 754舍入模式 预测舍入错误,并纠正它们。由于与其他舍入模式相比其相对误差较小,因此舍入到最接近的偶数位(在最后一位)是IEEE-754的默认舍入模式。

请注意默认的舍入模式,舍入到最接近 在最后一个地方甚至数字,保证一次操作的最后一个位置的误差小于一个单位的一半。单独使用截断,向​​上舍入和向下舍入可能会导致错误大于最后一个位置的一个单位的一半,但在最后一个位置时小于一个单位,因此不建议使用这些模式,除非它们是用于区间算术。

6.总结

简而言之,浮点运算中的错误的根本原因是硬件中的截断和在除法的情况下截断倒数的组合。由于IEEE-754标准在单个操作中仅需要小于一个单元的一半的误差,因此除非经过校正,否则重复操作的浮点误差将相加。


490
2018-02-25 21:43



(3)错了。分裂中的舍入误差不小于 一 单位在最后一个地方,但最多 半 最后一个单位。 - gnasher729
@ gnasher729好抓。使用默认的IEEE舍入模式,大多数基本操作在最后一个位置的误差小于1/2。编辑了解释,并注意到如果用户覆盖默认的舍入模式,错误可能大于一个ulp的1/2但小于1 ulp(在嵌入式系统中尤其如此)。 - KernelPanik
(1)浮点 数字 没有错误。每个浮点值都与它完全相同。大多数(但不是全部)浮点数 操作 给出不准确的结果。例如,没有二进制浮点值恰好等于1.0 / 10.0。一些操作(例如,1.0 + 1.0) 做 另一方面,给出准确的结果。 - james large
“浮点除法误差的主要原因是用于计算商的除法算法”是a 非常 误导性的话要说。对于符合IEEE-754标准的部门, 只要 浮点除法中的错误原因是结果无法在结果格式中准确表示;无论使用何种算法,都会计算相同的结果。 - Stephen Canon
@Matt很抱歉迟到的回复。这主要是由于资源/时间问题和权衡。有一种方法可以进行长划分/更“正常”划分,它被称为SRT划分,基数为2。然而,这会重复移位并从除数中减去除数并占用许多时钟周期,因为它只计算每个时钟周期的一个商。我们使用倒数表,以便我们可以计算每个周期的更多位商,并进行有效的性能/速度权衡。 - KernelPanik


当您将.1或1/10转换为基数2(二进制)时,您会在小数点后得到重复模式,就像尝试在基数10中表示1/3一样。值不准确,因此您无法做到使用常规浮点方法精确数学。


357
2017-11-20 02:39



伟大而简短的回答。重复模式看起来像0.00011001100110011001100110011001100110011001100110011 ... - Konstantin Chernov
这并不能解释为什么不是一个更好的算法,它不会首先转换成二进制文件。 - Dmitri Zaitsev
因为表现。使用二进制文件要快几千倍,因为它是机器的原生代码。 - Joel Coehoorn
有些方法可以产生精确的十进制值。 BCD(二进制编码的十进制)或其他各种形式的十进制数。但是,这些都比使用二进制浮点更慢(比较慢)并且占用更多存储空间。 (例如,打包的BCD在一个字节中存储2个十进制数字。这是一个字节中可能存储256个可能值的100个可能值,或100/256,这浪费了大约60%的一个字节的可能值。) - Duncan C
@Jacksonkr你还在考虑基地10。计算机是基础2。 - Joel Coehoorn


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

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

披萨刀具有非常精细的动作,如果你从一个完整的披萨开始,然后将其减半,并且每次继续将最小的切片减半,则可以减半 53次 在切片太小之前,甚至它的高精度能力。此时,您不能再将那个非常薄的切片减半,但必须按原样包含或排除它。

现在,你将如何将所有切片分成几乎十分之一(0.1)或五分之一(0.2)的披萨?真的想一想,试试吧。如果您手边有神话般的精密披萨刀,您甚至可以尝试使用真正的披萨。 :-)


当然,大多数有经验的程序员都知道真正的答案,即没有办法拼凑出来 精确 使用这些切片的披萨的十分之一或五分之一,无论你切成薄片多么细致。你可以做一个非常好的近似,如果你用近似值0.2加上0.1的近似值,你会得到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。


附:一些编程语言也提供可以的披萨切割器 将切片分成精确的十分之一。虽然这种披萨切割器并不常见,但如果您确实可以使用它,那么在重要的是能够获得切片的十分之一或五分之一时,应该使用它。

(最初发布在Quora上。)


226
2018-02-25 21:41



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


浮点舍入错误。由于缺少素数因子5,0.1不能在base-2中准确地表示为基数为10.正如1/3采用无穷多位数来表示十进制,但在base-3中为“0.1”, 0.1在base-2中采用无数个数字,而不是在base-10中。计算机没有无限的内存。


199
2018-04-09 12:25



计算机不需要无限量的内存就可以得到0.1 + 0.2 = 0.3 - Pacerier
@Pacerier当然,他们可以使用两个无界精度整数来表示一个分数,或者他们可以使用引号表示法。这是“二进制”或“十进制”的特定概念,这使得这不可能 - 你有一个二进制/十进制数字序列的想法,并在那里的某个地方,一个小数点。为了获得精确的理性结果,我们需要更好的格式。 - Devin Jeanpierre
@Pacerier:二进制和十进制浮点都不能精确存储1/3或1/13。十进制浮点类型可以精确地表示形式M / 10 ^ E的值, 但在表示大多数其他分数时,它们的精确度要比类似大小的二进制浮点数精确。在许多应用中,使用任意分数获得更高精度比使用一些“特殊”分数具有完美精度更有用。 - supercat
@Pacerier他们 做如果他们将数字存储为二进制浮点数,这就是答案的要点。 - Mark Amery
@chux:二进制和十进制类型之间的精度差异并不大,但十进制类型的最佳情况与最差情况精度的10:1差异远远大于二进制类型的2:1差异。我很好奇是否有人建立了硬件或编写的软件来有效地操作任何一种十进制类型,因为它们似乎都不适合在硬件或软件中有效实现。 - supercat


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

例如:

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

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


1 道格拉斯·克罗克福德: JavaScript:好的部分:附录A - 可怕部件(第105页)


99
2018-02-23 17:15



问题是转换本身是不准确的。 16.08 * 100 = 1607.9999999999998。我们是否必须分开编号并单独转换(如16 * 100 + 08 = 1608)? - Jason
这里的解决方案是以整数进行所有计算,然后除以您的比例(在这种情况下为100)并仅在呈现数据时进行舍入。这将确保您的计算始终精确。 - David Granado
只是为了挑选一点:整数算术只能在浮点数到达一个点(双关语)。如果数字大于0x1p53(使用Java 7的十六进制浮点表示法,= 9007199254740992),则此时ulp为2,因此0x1p53 + 1向下舍入为0x1p53(并且0x1p53 + 3向上舍入为0x1p53 + 4,因为圆到均匀)。 :-D当然,如果你的数字小于9千万亿,你应该没问题。 :-P - Chris Jester-Young
那你怎么样 .1 + .2 以显示 .3? - CodyBugstein
杰森,你应该绕结果(int)(16.08 * 100 + 0.5) - Mikhail Semenov


我的回答很长,所以我把它分成了三个部分。由于问题是浮点数学,我把重点放在机器的实际功能上。我还使其特定于双精度(64位),但该参数同样适用于任何浮点运算。

前言

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

value =(-1)^ s *(1.m5150...,M2102 * 2E-1023

64位:

  • 第一位是 标志位1 如果数字是负数, 0 除此以外1
  • 接下来的11位是 指数,是的 抵消 换句话说,在从双精度数读取指数位之后,必须减去1023以获得2的幂。
  • 剩下的52位是 尾数 (或尾数)。在尾数中,'暗示' 1. 总是2 省略,因为任何二进制值的最高位是 1

1  - IEEE 754允许a的概念 签署零  - +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版本)

卡托约翰斯顿 (问题提问者)问为什么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的二进制位数 十进制数字

将指数转换为十进制,删除偏移量,并重新添加隐含的 1 (在方括号中),0.1和0.2是:

0.1 = 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 = 2^-3 * [1].1001100110011001100110011001100110011001100110011010

要添加两个数字,指数必须相同,即:

0.1 = 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 = 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum = 2^-3 * 10.0110011001100110011001100110011001100110011001100111

由于总和不是2的形式ñ * 1. {bbb}我们将指数增加1并移动小数(二进制)得到:

sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)

尾数中现在有53位(第53位在上面的行中的方括号中)。默认值 舍入模式 对于IEEE 754是'回合最近' - 即如果是一个数字 X 介于两个值之间 一个 和 b,选择最低有效位为零的值。

a = 2^-2 * 1.0011001100110011001100110011001100110011001100110011
x = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
b = 2^-2 * 1.0011001100110011001100110011001100110011001100110100

注意 一个 和 b 仅在最后一位有所不同; ...0011 + 1 = ...0100。在这种情况下,最低有效位为零的值为 b,所以总和是:

sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100

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


81
2018-03-16 05:27



我的答案在发布后不久就被投了票。我已经做了很多改动(包括在二进制写入0.1和0.2时明确注意到重复的位,我在原文中省略了)。如果下选人看到这个的可能性,请你给我一些反馈,以便我能改进我的答案?我觉得我的答案增加了一些新内容,因为在其他答案中,IEEE 754中的总和处理方法没有以同样的方式涵盖。虽然“每个计算机科学家应该知道什么......”涵盖了一些相同的材料,但我的回答是成功的 特别 情况为0.1 + 0.2。 - Wai Ha Lee