请考虑以下代码:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
为什么会出现这些不准确之处?
请考虑以下代码:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
为什么会出现这些不准确之处?
二进制 浮点 数学是这样的。在大多数编程语言中,它基于 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。
我相信自从我设计和构建浮点硬件以来,我应该为此添加一个硬件设计师的视角。了解错误的起源可能有助于理解软件中发生的事情,最终,我希望这有助于解释为什么浮点错误发生并且似乎随着时间的推移而积累的原因。
从工程角度来看,大多数浮点运算都会有一些错误因素,因为执行浮点计算的硬件只需要在最后一个位置的误差小于一个单位的一半。因此,许多硬件将以精确度停止,该精度仅在最后一个位置产生小于一个单元的一半的误差所必需的 单一操作 这在浮点除法中尤其成问题。单个操作的构成取决于该单元占用的操作数。对于大多数情况,它是两个,但有些单位需要3个或更多操作数。因此,不能保证重复操作会导致所需的错误,因为错误会随着时间的推移而增加。
大多数处理器遵循 IEEE-754 标准但有些使用非规范化或不同的标准 。例如,在IEEE-754中存在非规范化模式,其允许以精度为代价来表示非常小的浮点数。然而,以下内容将涵盖IEEE-754的标准化模式,这是典型的操作模式。
在IEEE-754标准中,只要硬件设计者在最后一个地方不到一个单位的一半,就允许任何错误/ epsilon值,并且结果只需要小于最后一个单位的一半。一次操作的地方。这解释了为什么当重复操作时,错误加起来。对于IEEE-754双精度,这是第54位,因为53位用于表示浮点数的数字部分(标准化),也称为尾数(例如5.3e5中的5.3)。接下来的部分将详细介绍各种浮点运算的硬件错误原因。
浮点除法误差的主要原因是用于计算商的除法算法。大多数计算机系统使用乘法乘法来计算除法,主要是在 Z=X/Y
, Z = X * (1/Y)
。迭代地计算除法,即每个周期计算商的一些比特直到达到所需的精度,对于IEEE-754,最后一个地方的误差小于一个单位。 Y(1 / Y)的倒数表被称为慢除法中的商选择表(QST),商选择表的位大小通常是基数的宽度,或者是位数的比特数。在每次迭代中计算的商,加上一些保护位。对于IEEE-754标准,双精度(64位),它将是分频器的基数的大小,加上一些保护位k,其中 k>=2
。因此,例如,一次计算商的2位(基数4)的分频器的典型商数选择表将是 2+2= 4
位(加上几个可选位)。
3.1除法舍入误差:倒数近似
商选择表中的倒数取决于 分裂方法:SRT师等慢分工,或Goldschmidt师等快速分工;根据除法算法修改每个条目以试图产生尽可能低的错误。但无论如何,所有的倒数都是 近似值 实际的倒数和引入一些错误的元素。慢速分割和快速分割方法都迭代地计算商,即每一步计算商的一些位数,然后从被除数中减去结果,并且除法器重复这些步骤直到误差小于一半单位在最后一个地方。慢速划分方法在每个步骤中计算商的固定位数,并且通常构建成本较低,并且快速划分方法计算每步的可变位数并且通常构建成本更高。除法方法中最重要的部分是它们中的大多数都依赖于重复乘法 近似 相互的,所以他们容易出错。
所有操作中舍入错误的另一个原因是IEEE-754允许的最终答案的截断模式不同。有截断,圆向零, 圆到最近(默认), 向上舍入,向上舍入。对于单个操作,所有方法在最后位置引入小于一个单元的误差元素。随着时间的推移和重复的操作,截断也会累积地增加结果误差。这种截断误差在求幂中尤其成问题,它涉及某种形式的重复乘法。
由于执行浮点计算的硬件仅需要产生一个结果,错误小于单个操作的最后一个单位的一半,如果没有观察,错误将在重复操作上增加。这就是在需要有界误差的计算中,数学家使用诸如使用舍入到最近的方法的原因 在最后一个地方甚至数字 IEEE-754,因为随着时间的推移,错误更容易相互抵消,并且 区间运算 结合的变化 IEEE 754舍入模式 预测舍入错误,并纠正它们。由于与其他舍入模式相比其相对误差较小,因此舍入到最接近的偶数位(在最后一位)是IEEE-754的默认舍入模式。
请注意默认的舍入模式,舍入到最接近 在最后一个地方甚至数字,保证一次操作的最后一个位置的误差小于一个单位的一半。单独使用截断,向上舍入和向下舍入可能会导致错误大于最后一个位置的一个单位的一半,但在最后一个位置时小于一个单位,因此不建议使用这些模式,除非它们是用于区间算术。
简而言之,浮点运算中的错误的根本原因是硬件中的截断和在除法的情况下截断倒数的组合。由于IEEE-754标准在单个操作中仅需要小于一个单元的一半的误差,因此除非经过校正,否则重复操作的浮点误差将相加。
当您将.1或1/10转换为基数2(二进制)时,您会在小数点后得到重复模式,就像尝试在基数10中表示1/3一样。值不准确,因此您无法做到使用常规浮点方法精确数学。
这里的大多数答案都以非常干燥的技术术语来解决这个问题我想以正常人能够理解的方式来解决这个问题。
想象一下,你正在尝试切片比萨饼。你有一个可以削减披萨片的机器人披萨刀 究竟 一半。它可以将整个披萨减半,或者它可以将现有切片减半,但无论如何,减半总是精确的。
披萨刀具有非常精细的动作,如果你从一个完整的披萨开始,然后将其减半,并且每次继续将最小的切片减半,则可以减半 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。
附:一些编程语言也提供可以的披萨切割器 将切片分成精确的十分之一。虽然这种披萨切割器并不常见,但如果您确实可以使用它,那么在重要的是能够获得切片的十分之一或五分之一时,应该使用它。
浮点舍入错误。由于缺少素数因子5,0.1不能在base-2中准确地表示为基数为10.正如1/3采用无穷多位数来表示十进制,但在base-3中为“0.1”, 0.1在base-2中采用无数个数字,而不是在base-10中。计算机没有无限的内存。
除了其他正确答案之外,您可能还需要考虑缩放值以避免浮点运算出现问题。
例如:
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页)。
我的回答很长,所以我把它分成了三个部分。由于问题是浮点数学,我把重点放在机器的实际功能上。我还使其特定于双精度(64位),但该参数同样适用于任何浮点运算。
前言
一个 IEEE 754双精度二进制浮点格式(binary64) 数字代表一些表格
value =(-1)^ s *(1.m51米50...,M2米1米0)2 * 2E-1023
64位:
1
如果数字是负数, 0
除此以外1。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最大 (最大的非正规数,尾数完全由其组成) 1
s)是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
:最后几位是四舍五入的。