考虑以下代码:
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
为什么会出现这些不准确的情况?0.1
,即1/10
)其分母不是二的幂无法精确表示。binary64
格式中的0.1
,表示可以精确地写成:
0.1000000000000000055511151231257827021181583404541015625
(十进制),或0x1.999999999999ap-4
(C99 hexfloat notation)。0.1
,即1/10
,可以精确地写成:
0.1
(十进制),或0x1.99999999999999...p-4
类似于C99 hexfloat表示法的模拟,其中...
表示无尽的9序列。普通的十进制(基于10)数字也有同样的问题,这就是为什么1/3这样的数字最终变成了0.333333333...旁注:所有位置(基于N)的数字系统都存在这个精度问题
附注:在编程中使用浮点数
实际上,这个精度问题意味着您需要使用舍入函数将浮点数舍入到您感兴趣的小数位数,然后再显示它们。
您还需要用允许一定容差量的比较来替换等式测试,也就是说:
不要写成这样if (x == y) { ... }
,而应该写成if (abs(x - y) < myToleranceValue) { ... }
。其中abs
是绝对值函数,myToleranceValue
需要根据你的具体应用程序来选择,这将与你准备允许多少"摇摆空间"以及你要比较的最大数字有很大关系(由于精度问题)。在你选择的语言中小心使用"epsilon"样式常量。这些可以用作容差值,但它们的有效性取决于你正在处理的数字的大小,因为大数计算可能会超过epsilon阈值。我认为我应该加入一个硬件设计师的视角,因为我设计和构建浮点硬件。了解错误的起源可能有助于理解软件中正在发生的事情,最终,我希望这有助于解释为什么浮点错误会发生并似乎随着时间累积。
从工程角度来看,大多数浮点运算都会有一些误差,因为执行浮点计算的硬件只需要具有小于最后一位的一半单位的误差。因此,许多硬件将在仅需要产生小于最后一位的一半单位误差的精度下停止进行单个操作,这在浮点除法中尤其成问题。什么构成单个操作取决于该单位接受多少个操作数。对于大多数人来说,是两个,但有些单位需要3个或更多的操作数。由于这个原因,不能保证重复操作会产生理想的误差,因为随着时间的推移,误差会累积。
Z=X/Y
,Z = 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除法这样的快速除法;每个条目都根据除法算法进行修改,以尝试产生最低可能的误差。无论如何,所有倒数都是实际倒数的近似值,并引入一定的误差。慢速和快速除法方法都是迭代计算商,即每步计算某些位数的商,然后从被除数中减去结果,并且重复这些步骤,直到误差小于最后一位的一半为止。慢速除法方法在每个步骤中计算固定数量的商位数,通常更便宜,而快速除法方法在每个步骤中计算可变数量的位数,通常更昂贵。除法方法的最重要部分是大多数方法都依赖于对倒数的近似值进行重复乘法,因此容易出现误差。
由于负责浮点数计算的硬件仅需要在单次操作中产生误差小于最后一位的半个单位的结果,如果不加注意地进行重复操作,则误差将随着时间增长。这就是为什么在需要有界误差的计算中,数学家使用诸如使用IEEE-754的最近偶数位四舍五入方法等方法,因为随着时间的推移,错误更有可能互相抵消,而区间算术结合IEEE 754舍入模式的变化来预测舍入误差并进行纠正。由于与其他舍入模式相比其相对误差较低,所以最近偶数位四舍五入是IEEE-754的默认舍入模式。
请注意,默认的舍入模式是四舍五入到最近的偶数位, 它保证了一个操作中误差小于最后一位的一半。仅使用截断、向上取整和向下取整可能会导致误差大于最后一位的一半但小于最后一位,因此这些模式不建议使用,除非在区间算术中使用。
简而言之,浮点运算中错误的根本原因是硬件中的截断以及在除法中倒数的截断的组合。由于IEEE-754标准仅要求单个操作的误差小于最后一位的一半,因此在重复操作中,浮点误差将累加,除非进行修正。
这里的问题和你在学校学习并日常使用的十进制表示法一样,只不过是针对二进制。
要理解这个问题,想象一下将1/3表示为十进制值。这是不可能准确地完成的!当你在小数点后面写上无限多个“3”时,世界已经末日了,所以我们只能写一些数字,并认为它足够准确。
同样地,在二进制(二进制)中, 1/10 (十进制0.1)不能被表示成一个“十进制”值;小数点后面会出现一个无限循环的模式。这个值是不精确的,因此你不能使用普通的浮点数方法进行精确的计算。就像对于十进制一样,还有其他的值也存在这个问题。
1/3*3 == 1
这样简单的运算在BCD数学中失败(求值为假),就像在纸上使用十进制除法时一样失败。 - Joooeey这里的大多数答案都用非常干燥、技术性的术语回答了这个问题。我想用普通人可以理解的方式来回答。
想象一下,你正在尝试切披萨。你有一个机器人披萨刀,可以将披萨切成完全相等的两半。它可以将整个披萨分成两半,也可以将现有的一片披萨切成两半,但无论如何,切割总是精确的。
这个披萨刀具有非常细微的运动能力,如果你从整个披萨开始,然后将其切成两半,并且每次都将最小的那片再次切成两半,你可以在切了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稍高的值。浮点取整误差。由于缺少5的质因子,0.1在二进制中不能像十进制中那么精确地表示。就像1/3在十进制中需要无限数量的数字来表示,但在三进制中是“0.1”一样,在二进制中,0.1需要无限数量的数字,而在十进制中则不需要。而计算机没有无限的内存。
1
,否则为0
1。1.
始终被省略,因为任何二进制值的最高有效位都是1
2。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一样在有限数量的二进制位中精确表示。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
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
...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...
0.1 + 0.2
会得到0.3
的原因:最后几位四舍五入。除了其他正确的答案,您可能还希望考虑将值缩放以避免浮点算术问题。
例如:
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页)。
简而言之,原因如下:
浮点数在二进制中无法精确表示所有小数
就像10除以3在十进制中无法精确表示一样(它将是3.33...循环),同样地,1除以10在二进制中也无法精确表示。
那又怎样?如何处理这个问题? 有没有什么解决办法?
为了提供最佳解决方案,我可以说我发现了以下方法:
parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3
0.22 + 0.7 = 0.9199999999999999
toFixed(2)
,那么如何设置参数来适应每个给定的浮点数呢?(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"
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
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
。0.1
的地方已经出错了。 - undefined浮点数舍入误差。引自《计算机科学家应该知道的浮点运算》:
将无限多的实数塞进有限的比特中需要进行近似表示。虽然存在无限多个整数,但在大多数程序中,整数运算的结果可以存储在32位中。相比之下,对于给定任意数量的比特,大多数实数计算都将产生不能用那么多位完全表示的量。因此,浮点计算的结果通常必须进行舍入以适应其有限的表示形式。这种舍入误差是浮点计算的特征。