考虑以下代码:
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标准。问题的关键在于,数字以这种格式表示为整数乘以2的幂;分母不是2的幂的有理数(如0.1,即1/10)无法精确表示。
对于标准binary64格式的0.1,表示形式可以完全写为
0.1000000000000000055511151231257827021181583404541015625(十进制),或0x1.999999999999ap-4,采用C99六进制浮点数表示法。
相比之下,有理数0.1(1/10)可以完全写成
0.1(十进制),或0x1.999999999999999…p-4,类似于C99十六进制浮点数,其中。。。表示9的无限序列。
程序中的常数0.2和0.3也将近似于其真实值。恰好最接近0.2的两倍大于有理数0.2,但最接近0.3的两倍小于有理数0.3。0.1和0.2的和最终大于有理数0.3,因此与代码中的常数不一致。
浮点运算问题的一个相当全面的处理是每个计算机科学家都应该知道的浮点运算。有关更容易理解的解释,请参阅floatingpoint-gui.de。
边注:所有位置(以N为基数)数字系统都有精度问题
普通的十进制(以10为基数)数字也有同样的问题,这就是为什么像1/3这样的数字最终会变成0.33333333。。。
您刚刚偶然发现了一个数字(3/10),它很容易用十进制表示,但不适合二进制。它也是双向的(在某种程度上):1/16在十进制中是一个丑陋的数字(0.0625),但在二进制中,它看起来和十进制中的第10000个一样整洁(0.0001)**-如果我们在日常生活中习惯使用基数为2的数字系统,你甚至会看着这个数字,本能地理解你可以通过将某个数字减半,一次又一次地减半来达到这个目的。
当然,这并不是浮点数在内存中的存储方式(它们使用了一种科学的表示法)。然而,它确实说明了一点,二进制浮点精度错误往往会出现,因为我们通常感兴趣的“真实世界”数字往往是十的幂,但这只是因为我们每天使用十进制数字系统。这也是为什么我们会说71%而不是“每7取5”(71%是一个近似值,因为5/7不能用任何小数精确表示)。
所以不:二进制浮点数并没有被破坏,它们只是碰巧和其他N进制一样不完美:)
边注:在编程中使用浮点
实际上,这种精度问题意味着在显示浮点数之前,需要使用舍入函数将浮点数舍入到您感兴趣的小数位数。
您还需要用允许一定公差的比较来替换相等测试,这意味着:
如果(x==y){…}则不执行
相反,如果(abs(x-y)<myToleranceValue){…},则执行此操作。
其中abs是绝对值。需要为您的特定应用程序选择myToleranceValue,这与您准备允许多少“摆动空间”以及您将要比较的最大值(由于精度损失问题)有很大关系。当心您选择的语言中的“epsilon”样式常量。这些值可以用作公差值,但它们的有效性取决于您使用的数字的大小,因为使用大数字的计算可能会超过epsilon阈值。
浮点舍入错误。由于缺少5的素因子,0.1在基-2中不能像在基-10中那样精确地表示。正如1/3以十进制表示需要无限位数,但以3为基数表示为“0.1”,0.1以2为基数表示,而以10为基数不表示。计算机没有无限的内存。
浮点舍入错误。从每个计算机科学家应该知道的浮点运算:
将无限多的实数压缩成有限位数需要近似表示。虽然有无限多的整数,但在大多数程序中,整数计算的结果可以存储在32位中。相反,给定任何固定位数,大多数使用实数的计算将产生无法使用那么多位数精确表示的量。因此,浮点计算的结果必须经常舍入,以适应其有限表示。这种舍入误差是浮点计算的特征。
它被打破的方式与你在小学学习并每天使用的十进制(以10为基础)表示法完全相同,只是以2为基础。
要理解,请考虑将1/3表示为十进制值。这是不可能做到的!世界将在你写完小数点后的3之前结束,所以我们写了一些地方,认为它足够准确。
以同样的方式,1/10(十进制0.1)不能以2为基数(二进制)精确地表示为“十进制”值;小数点后的重复模式将永远持续下去。该值不精确,因此无法使用常规浮点方法对其进行精确计算。与基数10一样,还有其他值也显示了这个问题。
除了其他正确答案之外,您可能还需要考虑缩放值以避免浮点运算的问题。
例如:
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页)。
我的解决方法:
function add(a, b, precision) {
var x = Math.pow(10, precision || 2);
return (Math.round(a * x) + Math.round(b * x)) / x;
}
精度是指在加法过程中要保留小数点后的位数。
你试过胶带解决方案了吗?
尝试确定错误发生的时间,并用简短的if语句修复它们,这并不漂亮,但对于某些问题,这是唯一的解决方案,这就是其中之一。
if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
else { return n * 0.1 + 0.000000000000001 ;}
我在c#的一个科学模拟项目中也遇到过同样的问题,我可以告诉你,如果你忽视蝴蝶效应,它会变成一条大胖龙,咬你一口**
硬件设计师的视角
我认为,既然我设计并构建了浮点硬件,我就应该添加一个硬件设计师的视角。了解错误的来源可能有助于了解软件中发生的情况,最终,我希望这有助于解释为什么浮点错误会发生并似乎会随着时间累积的原因。
1.概述
从工程角度来看,大多数浮点运算都会有一些误差,因为进行浮点运算的硬件只需要在最后一个位置的误差小于一个单位的一半。因此,许多硬件将停止在一个精度上,该精度只需要在单个操作的最后位置产生小于一个单位的一半的误差,这在浮点除法中尤其有问题。什么构成一个操作取决于该单元需要多少个操作数。大多数情况下,它是两个,但有些单位需要3个或更多操作数。因此,不能保证重复操作会导致期望的错误,因为错误会随着时间的推移而增加。
2.标准
大多数处理器遵循IEEE-754标准,但有些处理器使用非规范化或不同的标准例如,IEEE-754中存在一种非规范化模式,该模式允许以精度为代价表示非常小的浮点数。然而,下面将介绍IEEE-754的标准化模式,这是典型的操作模式。
在IEEE-754标准中,硬件设计者可以使用误差/ε的任何值,只要它在最后一个位置小于一个单位的一半,并且一次操作的结果只需要在最后一位小于一个单元的一半。这解释了为什么当重复操作时,错误会增加。对于IEEE-754双精度,这是第54位,因为53位用于表示浮点数的数字部分(标准化),也称为尾数(例如5.3e5中的5.3)。下一节将更详细地介绍各种浮点操作的硬件错误原因。
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除法;根据除法算法修改每个条目,以尝试产生最小的可能误差。然而,在任何情况下,所有的倒数都是实际倒数的近似值,并引入了一些误差因素。慢除法和快除法都迭代地计算商,即每一步计算商的一些位数,然后从被除数中减去结果,除法器重复这些步骤,直到误差小于最后一个单位的一半。慢除法计算每一步的商的固定位数,通常构建成本较低,而快除法计算每步的位数可变,构建成本通常较高。除法中最重要的部分是,它们大多依赖于通过倒数的近似值进行重复乘法,因此容易出错。
4.其他操作中的舍入错误:截断
所有操作中舍入误差的另一个原因是IEEE-754允许的最终答案的不同截断模式。有截断、向零舍入、向最接近(默认)舍入、向下舍入和向上舍入。所有方法都会在单个操作的最后位置引入小于一个单位的误差元素。随着时间的推移和重复操作,截断也会累积地增加结果误差。这种截断误差在涉及某种形式的重复乘法的求幂运算中尤其有问题。
5.重复操作
由于执行浮点计算的硬件只需要在单个操作的最后一个位置产生误差小于一个单位的一半的结果,因此如果不注意,误差将随着重复操作而增加。这就是为什么在需要有界误差的计算中,数学家使用诸如在IEEE-754的最后一位使用舍入到最接近的偶数位的方法,因为随着时间的推移,误差更可能相互抵消,而区间算术结合了IEEE754舍入模式的变化来预测舍入误差,并对其进行校正。由于与其他舍入模式相比,其相对误差较低,因此舍入到最近的偶数位(最后一位)是IEEE-754的默认舍入模式。
请注意,默认舍入模式(舍入到最后一位最接近的偶数位)保证一次操作的误差小于最后一位单位的一半。仅使用截断、向上舍入和向下舍入可能会导致误差大于最后一位一个单位的一半,但小于最后一位的一个单位,因此不建议使用这些模式,除非它们用于区间算术。
6.总结
简而言之,浮点运算中出现错误的根本原因是硬件中的截断和除法中倒数的截断。由于IEEE-754标准只要求单个操作的误差小于最后一位一个单位的一半,因此重复操作中的浮点误差将相加,除非得到纠正。
这些奇怪的数字之所以出现,是因为计算机使用二进制(以2为基数)数字系统进行计算,而我们使用十进制(以10为基数)。
大多数分数不能用二进制或十进制或两者精确表示。结果-四舍五入(但精确)的数字结果。
已经发布了很多好的答案,但我想再补充一个。
并非所有数字都可以通过浮点数/双精度表示例如,在IEEE754浮点标准中,数字“0.2”将以单精度表示为“0.200000003”。
用于在引擎盖下存储实数的模型将浮点数表示为
即使您可以轻松键入0.2,FLT_RADIX和DBL_RADIX都是2;对于使用“IEEE二进制浮点运算标准(ISO/IEC Std 754-1985)”的带有FPU的计算机,不是10。
所以准确地表示这些数字有点困难。即使在没有任何中间计算的情况下显式指定此变量。
这里的大多数答案都用非常枯燥的技术术语来解决这个问题。我想用正常人能够理解的方式来解决这个问题。
想象一下,你正试图把披萨切成薄片。你有一个机器人披萨切割机,可以将披萨切成两半。它可以将整个披萨减半,也可以将现有的披萨减半,但无论如何,减半总是准确的。
那台披萨切割机动作非常精细,如果你从一整块披萨开始,然后将其减半,然后继续每次将最小的披萨片减半,你可以在披萨片太小甚至无法实现高精度功能之前,将其减半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.0200000000000000011102230246251565404236316680908203125=0.30000000000000000444089209850062616169452667236328125,而最接近0.3的数字实际上是0.29999999999988897769753748434595763683319091796875。
另外,一些编程语言还提供了披萨切割机,可以将披萨切成十分之一。虽然这种披萨切刀并不常见,但如果你有机会切到一个,那么你应该在切到十分之一或五分之一的披萨片非常重要的时候使用它。
(最初发布在Quora上。)
一些统计数据与这个著名的双精度问题有关。
当使用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%)。
我的答案很长,所以我把它分成了三部分。因为这个问题是关于浮点数学的,所以我把重点放在了机器的实际功能上。我还将其指定为双精度(64位),但该参数同样适用于任何浮点运算。
序言
IEEE 754双精度二进制浮点格式(binary64)数字表示以下形式的数字
值=(-1)^s*(1.m51m50…m2m1m0)2*2e-1023
64位:
第一位是符号位:如果数字为负,则为1,否则为0。接下来的11位是指数,偏移1023。换句话说,在从双精度数字中读取指数位之后,必须减去1023以获得2的幂。剩下的52位是有效位(或尾数)。在尾数中,“隐含”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-21-23-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的二进制位都可以用十进制数字精确地表示。
还要注意,我们可以将指数的幂减小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},我们将指数增加1,并移动小数(二进制)点以获得:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
= 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875
现在尾数中有53位(第53位在上一行的方括号中)。IEEE 754的默认舍入模式是“舍入到最近”,即如果数字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
注意,a和b仅在最后一位不同。。。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;博士
将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:最后几位是四舍五入的。
鉴于没有人提到这一点。。。
一些高级语言(如Python和Java)提供了克服二进制浮点限制的工具。例如:
Python的十进制模块和Java的BigDecimal类,它们在内部使用十进制表示法(与二进制表示法相反)表示数字。两者都有有限的精度,因此它们仍然容易出错,但它们解决了二进制浮点运算中最常见的问题。小数在处理金钱时很好:10美分加20美分总是正好是30美分:>>> 0.1 + 0.2 == 0.3错误>>>十进制('0.1')+十进制('0.2')==十进制('0.3')真的Python的十进制模块基于IEEE标准854-1987。Python的分数模块和Apache Common的BigFraction类。两者都将有理数表示为(分子、分母)对,它们可能给出比十进制浮点运算更精确的结果。
这两种解决方案都不是完美的(特别是如果我们考虑性能,或者如果我们需要非常高的精度),但它们仍然解决了二进制浮点运算的大量问题。
可以在数字计算机中实现的浮点数学必须使用实数的近似值及其运算。(标准版文件长达50多页,并有一个委员会处理其勘误表和进一步完善。)
这种近似是不同类型的近似的混合,每一种都可以被忽略或仔细考虑,因为其偏离精确性的特定方式。它还涉及到许多硬件和软件层面的明确例外情况,大多数人都会走过来假装没有注意到。
如果您需要无限精度(例如,使用数字π,而不是其许多较短的替代项之一),您应该编写或使用符号数学程序。
但是,如果您同意浮点数学有时在值和逻辑上是模糊的,错误可能会很快累积,并且您可以编写需求和测试来考虑这一点,那么您的代码可以经常通过FPU中的内容。
这个问题的许多重复问题都是关于浮点舍入对特定数字的影响。在实践中,通过查看感兴趣的计算的确切结果而不是仅仅阅读它,更容易了解它的工作原理。一些语言提供了实现这一点的方法,例如在Java中将浮点或双精度转换为BigDecimal。
由于这是一个语言不可知的问题,因此需要语言不可知工具,例如十进制到浮点转换器。
将其应用于问题中的数字,视为双精度:
0.1转换为0.1000000000000000055511151231257827021181583404541015625,
0.2转换为0.200000000000000011102230246251565404236316680908203125,
0.3转换为0.299999999999999988897769753748434595763683319091796875,以及
0.300000000000000004转换为0.30000000000000000444089209850062616169452667236328125。
手动或在十进制计算器(如Full Precision calculator)中添加前两个数字,显示实际输入的精确和为0.30000000000000000166533453693773481063544750213623046875。
如果四舍五入到等于0.3,则舍入误差将为0.000000000000000027755575615628913510591702705078125。四舍五入等于0.300000000000000004也会产生舍入误差0.000000000000000027755575615628913510591702705078125。打成平手的规则适用。
返回浮点转换器,0.300000000000000004的原始十六进制是3fd333333333334,以偶数结尾,因此是正确的结果。
不,不破,但大多数小数必须近似
总结
浮点运算是精确的,不幸的是,它与我们通常的以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硬件一些在基数10中看似简单但在基数2中却是重复的分数。
这是怎么发生的?
当我们用十进制书写时,每个分数(特别是每个终止的小数)都是形式的有理数
a/(2n x 5m)
在二进制中,我们只得到2n项,即:
a/2n
所以在十进制中,我们不能表示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,所有的数字都只使用2n项。
在base10中,我们不能表示1/3。但在二进制中,我们不能做1/10或1/3。
因此,虽然每一个二进制分数都可以用十进制来表示,但反过来却不正确。事实上,大多数小数在二进制中重复。
处理它
开发人员通常被要求进行<epsilon比较,更好的建议可能是舍入为整数值(在C库中:round()和round f(),即保持FP格式),然后进行比较。舍入到特定的小数部分长度可以解决大多数输出问题。
此外,在实数运算问题(FP是在早期昂贵的计算机上为之发明的问题)上,宇宙的物理常数和所有其他测量值只为相对较少的有效数字所知,因此整个问题空间无论如何都是“不精确的”。FP“精度”在这种应用中不是问题。
当人们尝试使用FP进行计数时,整个问题就真的出现了。它确实可以做到这一点,但前提是你坚持使用整数值,这会破坏使用它的意义。这就是为什么我们拥有所有这些小数软件库的原因。
我喜欢克里斯的披萨回答,因为它描述了实际问题,而不仅仅是关于“不准确”的通常手写。如果FP只是“不准确”,我们可以修复它,而且几十年前就已经做到了。我们没有这样做的原因是因为FP格式紧凑快速,是处理大量数字的最佳方式。此外,这也是太空时代和军备竞赛以及早期使用小型内存系统解决速度非常慢的计算机的大问题的尝试所留下的遗产。(有时,单个磁芯用于1位存储,但这是另一回事。)
结论
如果您只是在银行数豆子,那么首先使用十进制字符串表示的软件解决方案工作得非常好。但你不能这样做量子色动力学或空气动力学。
存储在计算机中的浮点数由两部分组成,一部分是整数,另一部分是基数乘以整数部分的指数。
如果计算机在基数为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.5是1 x 2⁻¹和0.25是1 x 2⁻²,将它们相加,结果为3 x 2⁻²或0.75。确切地
问题是数字可以精确地以10为基数表示,但不能以2为基数。这些数字需要四舍五入到最接近的相等值。假设非常常见的IEEE 64位浮点格式,最接近0.1的数字是3602879701896397 x 2⁻⁵⁵, 最接近0.2的数字是7205759403792794 x 2⁻⁵⁵; 将它们相加,得到10808639105689191 x 2⁻⁵⁵, 或精确的十进制值0.30000000000000000444089209850062616169452667236328125。浮点数通常四舍五入以显示。
我可以补充一下吗;人们总是认为这是一个计算机问题,但如果你用手(以10为基数)计算,你就不能得到(1/3+1/3=2/3)=真,除非你有无穷大可以将0.333…加到0.333……就像(1/10+2/10)一样==基数2的3/10问题,您将其截断为0.333+0.333=0.666,并可能将其舍入为0.667,这在技术上也是不准确的。
用三进制数,三分之三不是问题——也许有人会问为什么你的十进制数学被打破了。。。
为了好玩,我按照标准C99的定义玩了浮点数的表示,并编写了下面的代码。
代码以3个独立的组打印浮点的二进制表示
SIGN EXPONENT FRACTION
然后,它打印一个和,当以足够的精度求和时,它将显示硬件中真正存在的值。
因此,当你写float x=999…时,编译器会将该数字转换为函数xx打印的位表示,这样函数yy打印的和就等于给定的数字。
事实上,这个总数只是一个近似值。对于数字999999999,编译器将在浮点的位表示中插入数字1000000000
代码之后,我附加了一个控制台会话,在该会话中,我计算硬件中真正存在的两个常量(减去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中设置比例因子。
显示的金额是硬件内部的金额。通过计算它获得的值取决于设置的比例。我确实将比例因子设置为15。数学上,以无限的精度,它似乎是1000000000。
另一种方法是:使用64位来表示数字。因此,无法精确表示超过2**64=18446744073709551616个不同的数字。
然而,Math表示,在0和1之间已经有无限多的小数。IEE 754定义了一种编码,以有效地将这64位用于更大的数字空间加上NaN和+/-无穷大,因此在精确表示的数字之间存在间隙,只填充近似的数字。
不幸的是,0.3存在差距。
由于这篇文章对当前的浮点实现进行了一般性的讨论,我想补充一下,有一些项目正在解决它们的问题。
看看https://posithub.org/例如,它展示了一种称为posit(及其前身unum)的数字类型,它承诺以更少的比特提供更好的精度。如果我的理解是正确的,它也解决了问题中的问题。非常有趣的项目,背后的人是数学家约翰·古斯塔夫森博士。整个过程都是开源的,用C/C++、Python、Julia和C#实现了许多实际的实现(https://hastlayer.com/arithmetics).
简而言之,这是因为:
浮点数不能以二进制精确表示所有小数
因此,就像10/3不精确地存在于基数10中(它将是3.33……重复出现)一样,1/10也不存在于二进制中。
那又怎么样?如何处理?有什么解决办法吗?
为了提供最佳解决方案,我可以说我发现了以下方法:
parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3
让我解释一下为什么这是最好的解决方案。正如上面提到的其他答案一样,使用现成的Javascript toFixed()函数来解决问题是一个好主意。但很可能你会遇到一些问题。
假设你将两个浮点数相加,如0.2和0.7,这里是:0.2+0.7=0.8999999999999999。
您的预期结果是0.9,这意味着您需要一个精度为1位数的结果。因此,您应该使用(0.2+0.7).tfixed(1)但是不能只给toFixed()一个特定的参数,因为它取决于给定的数字,例如
0.22 + 0.7 = 0.9199999999999999
在本例中,您需要2位精度,因此它应该为Fixed(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));
}
让我们自己试试吧:函数floatify(数字){return parseFloat((number).toFixed(10));}函数addUp(){var number1=+$(“#number1”).val();var number2=+$(“#number2”).val();var expectedResult=number1+number2;var expectedResult=浮动(number1+number2);$(“#意外结果”).text(意外结果);$(“#expectedResult”).text(expectedResult);}addUp();输入{宽度:50px;}#预期结果{颜色:绿色;}#未预期结果{颜色:红色;}<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>预期结果:<span id=“expectedResult”></span></p><p>意外结果:<span id=“expectedResult”></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根本不起作用,尽管看起来是一样的!我更喜欢第一种解决方案,因为我可以将其作为一个函数应用,将输入浮点转换为精确的输出浮点。
仅供参考,乘法也存在同样的问题,例如0.09*10返回0.8999999999999999。应用flotify函数作为解决方法:flotify(0.09*10)返回0.9
从Python 3.5开始,您可以使用math.isclose()函数来测试近似相等性:
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
想象一下,以10为基数,例如8位数的精度工作。您检查是否
1/3 + 2 / 3 == 1
并了解到这返回错误。为什么?好吧,作为真实的数字
1/3=0.333….和2/3=0.666。。。。
在小数点后八位截断,我们得到
0.33333333 + 0.66666666 = 0.99999999
当然,这与1.00000000正好相差0.00000001。
具有固定位数的二进制数的情况完全类似。作为实数,我们有
1/10=0.0001100110011001100…(底座2)
and
1/5=0.00111001100110011001…(底座2)
如果我们把这些截成七位
0.0001100 + 0.0011001 = 0.0100101
而另一方面,
3/10=0.010011001100110011…(基数2)
被截断为七位的值为0.0100110,两者相差0.0000001。
确切的情况稍显微妙,因为这些数字通常以科学符号存储。因此,例如,我们可以将其存储为1.10011*2^-4,而不是将1/10存储为0.0001100,这取决于我们为指数和尾数分配了多少位。这会影响计算的精度位数。
结果是,由于这些舍入错误,您根本不想在浮点数上使用==。相反,您可以检查它们的差值的绝对值是否小于某个固定的小数字。
十进制数(如0.1、0.2和0.3)在二进制编码浮点类型中没有精确表示。0.1和0.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标准没有默认指定此类类型,而是作为技术报告中描述的扩展。
_Decimal32、_Decimal64和_Decimal128类型可能在您的系统上可用(例如,GCC在选定的目标上支持它们,但Clang在OS X上不支持它们)。
其实很简单。当你有一个基数为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/
我刚刚看到了关于浮点数的有趣问题:
考虑以下结果:
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
从维基百科的双精度浮点格式页面:
双精度二进制浮点是PC上常用的格式,因为它的范围比单精度浮点更广,尽管它的性能和带宽成本很高。与单精度浮点格式一样,与相同大小的整数格式相比,它缺少整数的精度。它通常简称为double。IEEE 754标准规定二进制64具有:符号位:1位指数:11位有效精度:53位(显式存储52位)具有给定偏置指数和52位分数的给定64位双精度数据假设的实际值为或
感谢@aguest向我指出了这一点。
在硬件级别,浮点数表示为二进制数的分数(以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中,浮点数操作中的错误是由底层硬件造成的,在大多数机器上,每次操作的错误率不超过1/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)
in :
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”。
正常的算术是以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) ...
浮点数的陷阱是它们看起来像十进制,但它们是二进制的。
2的唯一素因子是2,而10的素因子为2和5。这样做的结果是,每一个可以完全写成二进制分数的数字也可以完全写成十进制分数,但只有一部分可以写成十进制分数的数字可以写成二进制分数。
浮点数本质上是一个有效位数有限的二进制分数。如果你超过这些有效数字,那么结果将被四舍五入。
当您在代码中键入文字或调用函数将浮点数解析为字符串时,它需要一个十进制数,并将该十进制数的二进制近似值存储在变量中。
当您打印浮点数或调用函数将浮点数转换为字符串时,它将打印浮点数的十进制近似值。可以将二进制数字精确地转换为十进制,但在转换为字符串*时,我所知道的任何语言都不会默认这样做。一些语言使用固定数量的有效数字,其他语言使用最短的字符串,该字符串将“往返”返回到相同的浮点值。
*Python在将浮点数转换为“decimal.decimal”时确实会进行精确的转换。这是我所知道的获得浮点数的精确十进制等效值的最简单方法。