考虑以下代码:
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
为什么会出现这些错误?
Answers:
二进制浮点数学就是这样。在大多数编程语言中,它基于IEEE 754标准。问题的症结在于数字以这种格式表示为整数乘以2的幂。分母不是2的幂的有理数(例如0.1
,是1/10
)无法精确表示。
对于0.1
标准binary64
格式,表示形式可以完全按照
0.1000000000000000055511151231257827021181583404541015625
以十进制表示,或0x1.999999999999ap-4
以C99十六进制表示法表示。相比之下,合理数量0.1
,这是1/10
可以完全按照书面
0.1
以十进制表示,或0x1.99999999999999...p-4
以C99十六进制表示法的类似形式表示,其中...
表示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。
旁注:所有位置(以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”样式常量。这些不得用作公差值。
我相信我应该为此添加硬件设计师的观点,因为我设计并构建了浮点硬件。知道错误的来源可能有助于理解软件中发生的事情,并且最终,我希望这可以帮助解释为什么会出现浮点错误并随着时间的推移而累积的原因。
从工程的角度来看,大多数浮点运算都将具有一定的错误元素,因为进行浮点计算的硬件仅要求最后的误差小于一个单元的一半。因此,很多硬件将停止在这样的精度上,该精度仅对于单次操作在最后一次产生的误差小于一个单元的一半是必要的,这在浮点除法中尤其成问题。构成单个操作的要素取决于该单元采用的操作数。对于大多数情况,它是两个,但是某些单位使用3个或更多操作数。因此,不能保证重复操作会导致理想的错误,因为随着时间的推移这些错误会累加。
大多数处理器遵循IEEE-754标准,但有些处理器使用非规范化或不同的标准。例如,IEEE-754中存在一种非规范化模式,该模式允许以精度为代价表示非常小的浮点数。但是,以下内容将涵盖IEEE-754的标准化模式,这是典型的操作模式。
在IEEE-754标准中,只要最后一位小于一个单位的一半,并且允许硬件设计者允许任何误差/ε值,并且最后的结果必须小于一个单位的一半。一个手术的地方。这解释了为什么当重复操作时,错误加起来。对于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允许的最终答案截断的不同模式。有截断,四舍五入,四舍五入(默认值),四舍五入和四舍五入。对于单个操作,所有方法最后都会引入误差小于1个单位的元素。随着时间的流逝和重复的操作,截断还会累积地导致错误。截断误差在取幂时尤其成问题,涉及某种形式的重复乘法。
由于执行浮点计算的硬件只需要产生一个结果,该结果的单个操作的最后一个位置的误差小于一个单元的一半,因此如果不注意,该误差将随着重复的操作而扩大。这就是为什么在需要有限误差的计算中,数学家会使用诸如在 IEEE-754 的最后一位使用四舍五入到最接近的偶数之类的方法的原因,因为随着时间的流逝,误差更可能相互抵消。和间隔算术结合IEEE 754舍入模式的变体预测舍入误差并进行更正。由于与其他舍入模式相比其相对误差较低,因此舍入到最接近的偶数位(在最后一位)是IEEE-754的默认舍入模式。
请注意,默认的舍入模式(最后一位舍入到最接近的偶数位)保证一次操作的最后一位的误差小于一个单位的一半。单独使用截断,舍入和舍入可能会导致错误,该错误大于最后一个单元的一半,但小于最后一个单元,因此不建议使用这些模式,除非它们是在间隔算术中使用。
简而言之,浮点运算错误的根本原因是硬件的截断和除法时的倒数截断的组合。由于IEEE-754标准在一次操作中只要求最后一个位置的误差小于一个单元的一半,因此,除非进行纠正,否则重复操作中的浮点错误将加起来。
当您将.1或1/10转换为以2为基数(二进制)时,您会在小数点后得到一个重复模式,就像试图以10为基数表示1/3。该值不准确,因此您无法执行使用普通的浮点方法进行精确数学运算。
这里的大多数答案都是用非常干燥的技术术语来解决这个问题。我想以普通人可以理解的方式来解决这个问题。
想象一下,您正在尝试切比萨饼。你有一个机器人比萨刀,可以削减比萨正好一半。它可以将整个披萨减半,也可以将现有的薄片减半,但是无论如何,减半总是精确的。
比萨切刀的动作非常精细,如果您从整个比萨开始,则将其减半,然后每次将最小的薄片减半,则可以将薄片减半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一些编程语言还提供了比萨饼切割器,可以将切成精确的十分之一。尽管这种比萨饼切割器并不常见,但是如果您确实有机会使用它,那么在重要的是能够精确获得十分之一或五分之一的切片时,应该使用它。
浮点舍入错误。由于缺少素数5,所以0.1在base-2中不能像在base-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
该表达式在JavaScript中0.1 + 0.2 === 0.3
返回false
,但幸运的是,浮点数的整数运算是精确的,因此可以通过缩放避免十进制表示错误。
作为一个实际示例,为避免精度至高无上的浮点问题,建议1将货币作为代表美分数量的整数来处理:2550
美分而不是25.50
美元。
1 Douglas Crockford:JavaScript:优秀部分:附录A-糟糕的部分(第105页)。
我的回答很长,因此我将其分为三个部分。由于问题是关于浮点数学的,因此我将重点放在机器的实际功能上。我还专门针对双精度(64位)精度,但是该参数同样适用于任何浮点运算。
前言
一个IEEE 754双精度二进制浮点格式(binary64)数表示数字形式的
值=(-1)^ s *(1.m 51 m 50 ... m 2 m 1 m 0)2 * 2 e-1023
64位:
1
如果数字为负数,0
否则为1。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 最大值(最大的反规范数,其尾数完全由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版本)
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介于两个值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; 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
:最后几位是四舍五入的。
存储在计算机中的浮点数由两部分组成:一个整数和一个以整数为底并乘以该整数的指数。
如果计算机以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
is 1 x 2⁻¹
和0.25
is 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.3000000000000000444089209850062616169452667236328125
。浮点数通常会四舍五入以显示。
浮点舍入错误。从每位计算机科学家应该了解的浮点算法中:
将无限多个实数压缩为有限数量的位需要近似表示。尽管有无限多个整数,但是在大多数程序中,整数计算的结果可以存储在32位中。相反,在给定固定位数的情况下,大多数使用实数的计算将产生无法使用那么多位数精确表示的数量。因此,浮点计算的结果通常必须四舍五入,以重新适合其有限表示形式。舍入误差是浮点计算的特征。
一些统计数据与此著名的双精度问题有关。
当以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%)。
摘要
不幸的是,浮点运算是精确的,与我们通常的以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位存储,但这是另一回事了。)
结论
如果您只是在银行里数豆,那么首先使用十进制字符串表示形式的软件解决方案就可以很好地工作。但是您不能那样做量子色动力学或空气动力学。
nextafter()
对IEEE浮点数的二进制表示形式进行整数递增或递减实现。另外,您可以将浮点数作为整数进行比较,并获得正确的答案,除非它们均为负数(因为符号幅度与2的补码比较)。
为了提供最佳解决方案,我可以说我发现了以下方法:
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).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
尽管看起来一样,但它根本不会起作用!我更喜欢第一个解决方案,因为我可以将其用作将输入浮点数转换为准确的输出浮点数的函数。
这个问题的许多重复项中有许多都询问浮点取整对特定数字的影响。在实践中,通过查看感兴趣的计算的确切结果,而不是仅仅阅读它,会更容易感觉到它是如何工作的。有些语言提供了这样做的方式-如转换float
或double
以BigDecimal
Java编写的。
由于这是与语言无关的问题,因此需要与语言无关的工具,例如小数到浮点转换器。
将其应用于问题中的数字,视为双精度:
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,该数字以偶数结尾,因此是正确的结果。
鉴于没有人提及此事...
一些高级语言(例如Python和Java)附带了克服二进制浮点限制的工具。例如:
Python的decimal
模块和Java的BigDecimal
class,内部用十进制表示法表示数字(与二进制表示法相对)。两者的精度都有限,因此它们仍然容易出错,但是它们使用二进制浮点算法解决了最常见的问题。
处理货币时,小数非常好:十美分加二十美分总是正好是三十美分:
>>> 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的BigFraction
class。两者都将有理数表示为(numerator, denominator)
对,并且它们可能比十进制浮点算术给出更准确的结果。
这些解决方案都不是完美的(特别是如果我们考虑性能,或者需要非常高的精度),但是它们仍然使用二进制浮点算法解决了许多问题。
我可以补充吗?人们总是认为这是计算机问题,但是如果您用手计数(以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根手指会问为什么十进制数学运算被破坏了...
可以在数字计算机中实现的那种浮点数学运算必须使用实数的近似值及其上的运算。(标准版本运行多达五十页的文档,并设有一个委员会来处理其勘误表并作进一步完善。)
这种近似是不同类型的近似的混合,由于其与精确度的特定偏离方式,每种近似都可以忽略不计或仔细考虑。它还在硬件和软件级别上涉及到许多明显的例外情况,大多数人会假装不注意而已。
如果需要无限精度(例如,使用数字π代替许多更短的替代之一),则应编写或使用符号数学程序。
但是,如果您对有时浮点数学的值模糊并且逻辑和错误会迅速累积的想法感到满意,并且可以编写要求和测试以允许这样做,那么您的代码就可以经常使用其中的内容您的FPU。
只是为了好玩,按照标准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。
想象一下,以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
。这会影响您为计算获得的精度位数。
结果是由于这些舍入错误,您实际上根本不想在浮点数上使用==。而是可以检查它们的差的绝对值是否小于某个固定的小数。
从Python 3.5开始,您可以使用math.isclose()
函数测试近似相等:
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
由于该线程在当前浮点实现的一般讨论中有所分支,因此我补充说,有一些项目正在解决其问题。
以https://posithub.org/为例,它展示了一个称为posit(及其前身unum)的数字类型,该数字类型有望以更少的位数提供更高的准确性。如果我的理解是正确的,那么它也可以解决问题中的这类问题。这个项目非常有趣,其背后的人物是约翰·古斯塔夫森博士(John Gustafson)的数学家。整个过程都是开源的,在C / C ++,Python,Julia和C#(https://hastlayer.com/arithmetics)中有许多实际实现。
实际上很简单。当您拥有以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为基数的系统中重复小数。对这些重复的小数进行数学运算时,
十进制数如0.1
,0.2
,和0.3
不准确表示以二进制编码的浮点类型。和的近似值之0.1
和0.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)。
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接受任意数量的参数
考虑以下结果:
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向我指出这一点。