提问人:Cato Johnston 提问时间:2/26/2009 最后编辑:Donald DuckCato Johnston 更新时间:11/19/2023 访问量:561911
浮点数学坏了吗?
Is floating point math broken?
问:
请考虑以下代码:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
为什么会出现这些不准确之处?
答:
二进制浮点数学是这样的。在大多数编程语言中,它基于 IEEE 754 标准。问题的症结在于,数字以这种格式表示为整数乘以 2 的幂;分母不是 2 的幂的有理数(例如 ,即 )不能精确表示。0.1
1/10
因为在标准格式中,表示可以完全写成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。
旁注:所有位置(以 N 为基数)数字系统都精确地解决了这个问题
普通的旧十进制(以 10 为基数)数字也有同样的问题,这就是为什么像 1/3 这样的数字最终为 0.333333333......
你刚刚偶然发现了一个数字(3/10),它恰好很容易用十进制系统表示,但不适合二进制系统。它也是双向的(在某种程度上):1/16 在十进制 (0.0625) 中是一个丑陋的数字,但在二进制中,它看起来就像十进制 (0.0001) 中的第 10,000 个一样整洁** - 如果我们习惯于在日常生活中使用以 2 为基数的数字系统,你甚至会看着这个数字,并本能地理解你可以通过将某些东西减半来到达那里, 一次又一次地减半。
当然,这并不是浮点数在内存中的存储方式(它们使用一种科学记数法)。然而,它确实说明了二进制浮点精度误差往往会出现,因为我们通常感兴趣的“现实世界”数字通常是十的幂 - 但这只是因为我们每天使用十进制数字系统。这也是为什么我们会说 71% 而不是“每 7 个中有 5 个”(71% 是一个近似值,因为 5/7 不能用任何十进制数精确表示)。
所以不:二进制浮点数没有被破坏,它们只是碰巧和其他所有以 N 为基数的系统一样不完美:)
旁注:在编程中使用浮点数
在实践中,这种精度问题意味着您需要使用四舍五入函数将浮点数四舍五入到您感兴趣的小数位数,然后再显示它们。
您还需要将相等性测试替换为允许一定程度的容差的比较,这意味着:
不要做if (x == y) { ... }
取而代之的是 .if (abs(x - y) < myToleranceValue) { ... }
其中 是绝对值。 需要为您的特定应用选择 - 这与您准备允许的“回旋余地”以及您将要比较的最大数字(由于精度损失问题)有很大关系。请注意所选语言中的“epsilon”样式常量。这些值可以用作容差值,但其有效性取决于您正在处理的数字的大小(大小),因为使用大量数字的计算可能会超过 epsilon 阈值。abs
myToleranceValue
评论
浮点舍入误差。由于缺少质因数 5,因此 0.1 在以 2 为底的 2 中不能像在 10 为基数中那样准确表示。正如 1/3 需要无限数量的数字来表示十进制,但在基数 3 中是“0.1”一样,0.1 在基数 2 中采用无限数量的数字,而在基数 10 中则不然。而且计算机没有无限的内存。
评论
浮点舍入误差。根据每个计算机科学家都应该了解的浮点运算:
将无限多的实数压缩到有限位数的位中需要近似表示。尽管有无限多的整数,但在大多数程序中,整数计算的结果可以存储在 32 位中。相反,给定任何固定位数的位数,大多数使用实数的计算将产生无法用那么多位精确表示的数量。因此,浮点计算的结果通常必须四舍五入,以便重新适应其有限表示。这种舍入误差是浮点计算的特征。
它的破坏方式与您在小学学习并每天使用的十进制(以 10 为基数)符号被破坏的方式完全相同,仅适用于以 2 为基数。
要理解,请考虑将 1/3 表示为十进制值。这是不可能的!在你写完小数点后 3 之前,世界就结束了,所以我们写到一些地方并认为它足够准确。
同样,1/10(十进制 0.1)不能在基数 2(二进制)中精确表示为“十进制”值;小数点后的重复模式会永远持续下去。该值并不准确,因此您不能使用普通浮点方法对其进行精确数学运算。就像以 10 为基数一样,还有其他值也会出现这个问题。
评论
1/3*3 == 1
除了其他正确答案之外,您可能还需要考虑缩放值以避免浮点运算问题。
例如:
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 标准中,硬件设计人员可以使用任何误差/epsilon 值,只要它最后小于一个单元的一半,并且结果只需要小于最后一个单元的一半。这就解释了为什么当有重复操作时,错误会累积起来。对于 IEEE-754 双精度,这是第 54 位,因为 53 位用于表示浮点数的数字部分(归一化),也称为尾数(例如 5.3e5 中的 5.3)。接下来的部分将更详细地介绍各种浮点运算的硬件错误原因。
3. 除法中舍入误差的原因
浮点除法误差的主要原因是用于计算商的除法算法。大多数计算机系统使用乘法乘以倒数来计算除法,主要是在 、 中。除法是迭代计算的,即每个周期计算商的一些位,直到达到所需的精度,对于 IEEE-754 来说,这是误差小于一个单位的任何内容。Y(1/Y)的倒数表在慢除法中称为商选择表(QST),商选择表的大小(以位为单位)通常是基数的宽度,或每次迭代中计算的商的位数,加上几个保护位。对于 IEEE-754 标准,双精度(64 位),它将是分频器基数的大小,加上几个保护位 k,其中 .因此,例如,一次计算 2 位商(基数 4)的除法器的典型商选择表将是位(加上一些可选位)。Z=X/Y
Z = X * (1/Y)
k>=2
2+2= 4
3.1 除法舍入误差:倒数的近似值
商选择表中的倒数取决于除法:慢除法如SRT除法,或快除法如Goldschmidt除法;每个条目都根据除法算法进行修改,以尝试产生尽可能低的误差。然而,无论如何,所有倒数都是实际倒数的近似值,并引入了一些误差因素。慢除法和快除法都是迭代计算商,即每一步计算一定数量的商,然后从被除数中减去结果,除法器重复这些步骤,直到误差小于最后一个单位的一半。慢除法计算每一步中固定位数的商,通常构建成本较低,而快速除法计算每步可变位数,通常构建成本更高。除法最重要的部分是,它们中的大多数都依赖于重复乘法的倒数近似值,因此它们容易出错。
4. 其他操作中的舍入错误:截断
所有操作中舍入误差的另一个原因是 IEEE-754 允许的最终答案的不同截断模式。有截断、四舍五入到零、四舍五入到最接近(默认)、向下舍入和向上舍入。对于单个操作,所有方法都会在最后位置引入小于一个单位的误差元素。随着时间和重复操作的进行,截断也会累积增加由此产生的误差。这种截断误差在幂式运算中尤其成问题,幂式运算涉及某种形式的重复乘法。
5. 重复操作
由于执行浮点计算的硬件只需要在单个操作中产生误差小于一个单位的一半的结果,因此如果不注意,误差将在重复操作中增加。这就是为什么在需要有界误差的计算中,数学家使用的方法包括在 IEEE-754 的最后位置使用四舍五入到最接近的偶数,因为随着时间的推移,误差更有可能相互抵消,并且区间算术与 IEEE 754 舍入模式的变化相结合预测舍入错误,并纠正它们。由于与其他舍入模式相比,其相对误差较低,因此舍入到最接近的偶数(最后一位)是 IEEE-754 的默认舍入模式。
请注意,默认的舍入模式,即最后一位四舍五入到最接近的偶数,保证一次操作的误差小于最后一位单位的一半。单独使用截断、向上舍入和向下舍入可能会导致误差在最后一位大于一个单位的一半,但在最后一位小于一个单位,因此不建议使用这些模式,除非它们在区间算术中使用。
6. 总结
简而言之,浮点运算中出错的根本原因是硬件中的截断和除法情况下倒数的截断的结合。由于 IEEE-754 标准只要求单次操作的误差小于一个单位的一半,因此除非纠正,否则重复操作的浮点误差将加起来。
评论
之所以出现这些奇怪的数字,是因为计算机使用二进制(以 2 为基数)数字系统进行计算,而我们使用十进制(以 10 为基数)。
大多数小数不能用二进制或十进制或两者兼而有之精确表示。结果 - 四舍五入(但精确)的数字结果。
评论
并非所有数字都可以通过浮点数/双精度数表示。 例如,在 IEEE 754 浮点标准中,数字“0.2”将表示为单精度“0.200000003”。
在引擎盖下存储实数的模型将浮点数表示为
即使你可以很容易地打字,并且是 2;对于具有使用“IEEE 二进制浮点运算标准 (ISO/IEEE Std 754-1985)”的 FPU 的计算机,不是 10。0.2
FLT_RADIX
DBL_RADIX
因此,要准确表示这样的数字有点困难。即使您显式指定此变量而不进行任何中间计算。
这里的大多数答案都用非常枯燥的技术术语来解决这个问题。我想用正常人可以理解的术语来解决这个问题。
想象一下,你正在尝试切比萨饼。你有一个机器人披萨切割机,可以将披萨片切成两半。它可以将整个披萨减半,也可以将现有切片减半,但无论如何,减半总是准确的。
那台披萨切割机的动作非常精细,如果你从整个披萨开始,然后把它减半,然后每次都继续将最小的切片减半,你可以做减半 53 次,直到切片太小,甚至无法达到它的高精度能力。在这一点上,你不能再把这个非常薄的切片减半,而必须按原样包含或排除它。
现在,您将如何以这样一种方式将所有切片切成比萨饼的十分之一 (0.1) 或五分之一 (0.2)?认真考虑一下,并尝试解决它。如果您手头有一台神话般的精密披萨切割机,您甚至可以尝试使用真正的披萨。:-)
当然,大多数有经验的程序员都知道真正的答案,那就是无论你把它们切得多么细,都无法用这些切片拼凑出披萨的十分之一或五分之一。你可以做一个很好的近似,如果你把 0.1 的近似值和 0.2 的近似值相加,你会得到一个相当不错的 0.3 的近似值,但它仍然是一个近似值。
对于双精度数字(允许您将披萨减半 53 倍的精度),小于和大于 0.1 的数字是 0.099999999999999999999167332731531132594682276248931884765625 和 0.10000000000000000055511151231257827021181583404541015625。后者比前者更接近 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.10000000000000000055511151231257827021181583404541015625 + 0.20000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125,而最接近 0.3 的数字实际上是 0.2999999999999999998888897769753748434595763683319091796875。
P.S. 一些编程语言还提供披萨切割器,可以将切片分成精确的十分之一。虽然这样的披萨刀并不常见,但如果您确实可以使用披萨刀,那么当能够准确地获得切片的十分之一或五分之一很重要时,您应该使用它。
评论
一些统计数据与这个著名的双精度问题有关。
当使用 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...米 2 米1米0)2* 2E-1023
在 64 位中:
- 第一位是符号位:如果数字为负数,否则为1。
1
0
- 接下来的 11 位是指数,它被 1023 偏移。换句话说,从双精度数中读取指数位后,必须减去 1023 才能获得 2 的幂。
- 剩下的 52 位是有效(或尾数)。在尾数中,“隐含”总是省略2,因为任何二进制值的最高有效位是 。
1.
1
1 - IEEE 754 允许有符号零的概念 - 并且处理方式不同:是正无穷大; 是负无穷大。对于零值,尾数和指数位均为零。注意:零值(+0 和 -0)明确不归类为非正态2。+0
-0
1 / (+0)
1 / (-0)
2 - 非正态数并非如此,其偏移指数为零(隐含)。非正态双精度数的范围是 dmin ≤ |x|≤ d max,其中 dmin(最小的可表示非零数)为 2-1023 - 51 (≈ 4.94 * 10-324),dmax(最大的非正态数,尾数完全由 s 组成)为 2-1023 + 1 - 2-1023 - 51 (≈ 2.225 * 10-308)。0.
1
将双精度数转换为二进制
存在许多在线转换器将双精度浮点数转换为二进制(例如在 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
请注意,尾数由 的重复数字组成。这是计算存在任何错误的关键 - 0.1、0.2 和 0.3 不能在有限数量的二进制位中以二进制精确表示,超过 1/9、1/3 或 1/7 可以精确表示为十进制数字。0011
另请注意,我们可以将指数的幂减少 52,并将二进制表示中的点向右移动 52 位(很像 10-3 * 1.23 == 10-5 * 123)。然后,这使我们能够将二进制表示为它以 a * 2p 形式表示的确切值。其中“a”是整数。
将指数转换为小数,删除偏移量,并重新添加隐含的(在方括号中),0.1 和 0.2 为:1
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 仅在最后一位有所不同; + = .在本例中,最低有效位为零的值为 b,因此总和为:...0011
1
...0100
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;博士
以 IEEE 754 二进制表示形式编写(三个部分之间用冒号分隔)并将其与 进行比较,这是(我将不同的位放在方括号中):0.1 + 0.2
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 的
decimal
模块和 Java 的BigDecimal
类,在内部用十进制表示法(而不是二进制表示法)表示数字。两者的精度都有限,因此它们仍然容易出错,但是它们解决了二进制浮点运算的最常见问题。在处理金钱时,小数点非常好:十美分加二十美分总是正好三十美分:
>>> 0.1 + 0.2 == 0.3 False >>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3') True
Python 的模块基于 IEEE 标准 854-1987。
decimal
Python 的
fractions
模块和 Apache Common 的BigFraction
类。两者都将有理数表示为对,它们可能给出比十进制浮点运算更准确的结果。(numerator, denominator)
这些解决方案都不是完美的(特别是如果我们看性能,或者我们需要非常高的精度),但它们仍然解决了二进制浮点运算的大量问题。
评论
可以在数字计算机中实现的那种浮点数学必然使用实数的近似值和对它们的运算。(标准版本长达五十多页的文档,并有一个委员会来处理其勘误和进一步完善。
这种近似是不同种类的近似值的混合体,每一种近似值都可以被忽略或仔细解释,因为它偏离精确性的特定方式。它还涉及硬件和软件级别的许多明确的例外情况,大多数人假装没有注意到这些情况就走过了。
如果你需要无限的精度(例如,使用数字π,而不是它的许多较短的替身之一),你应该编写或使用符号数学程序。
但是,如果你同意有时浮点数学在值和逻辑上是模糊的,错误会迅速积累,并且你可以编写你的需求和测试来允许这一点,那么你的代码可以经常与你的 FPU 中的内容相处。
这个问题的众多重复项中有许多都询问了浮点舍入对特定数字的影响。在实践中,通过查看感兴趣的计算的确切结果而不是仅仅阅读它,更容易了解它是如何工作的。有些语言提供了执行此操作的方法 - 例如在 Java 中将 a 或 转换为。float
double
BigDecimal
由于这是一个与语言无关的问题,因此它需要与语言无关的工具,例如十进制到浮点转换器。
将其应用于问题中的数字,被视为双精度:
0.1 转换为 0.10000000000000000055511151231257827021181583404541015625,
0.2 转换为 0.2000000000000000011102230246251565404236316680908203125,
0.3 转换为 0.299999999999999988897769753748434595763683319091796875,并且
0.30000000000000004 转换为 0.30000000000000000444089209850062616169452667236328125。
手动或在十进制计算器(如全精度计算器)中将前两个数字相加,显示实际输入的确切总和为 0.300000000000000000166533453693773481063544750213623046875。
如果四舍五入到相当于 0.3,则舍入误差为 0.000000000000000000277555756156289135105907917022705078125。四舍五入到相当于 0.3000000000000000004 也会给出舍入误差 0.00000000000000000277555756156289135105907917022705078125。回合到偶的决胜局适用。
返回浮点转换器,0.3000000000000000004 的原始十六进制是 3fd333333333333334,它以偶数结尾,因此是正确的结果。
评论
不,没有损坏,但大多数小数必须是近似的
总结
浮点运算是精确的,不幸的是,它与我们通常的以 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 5米)
在二进制中,我们只得到 2n 项,即:
一个 / 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 项。
在以10 为基数中,我们不能表示 1/3。但是在二进制中,我们不能做 1/10 或 1/3。
因此,虽然每个二进制分数都可以用十进制书写,但反之则不然。事实上,大多数十进制分数都以二进制形式重复。
处理它
开发人员通常被指示进行< epsilon 比较,更好的建议可能是四舍五入到整数值(在 C 库中:round() 和 roundf(),即保持 FP 格式),然后进行比较。四舍五入到特定的小数部分长度可以解决大多数输出问题。
此外,在实数运算问题上(FP是在早期昂贵的计算机上发明的问题),宇宙的物理常数和所有其他测量值只有相对较少的有效数字知道,因此整个问题空间无论如何都是“不精确的”。FP“精度”在这种应用中不是问题。
当人们尝试使用 FP 进行豆子计数时,整个问题就出现了。它确实有效,但前提是你坚持整数值,这有点违背了使用它的意义。这就是为什么我们拥有所有这些十进制分数软件库的原因。
我喜欢 Chris 的 Pizza 回答,因为它描述了实际问题,而不仅仅是通常的关于“不准确”的挥手。如果 FP 只是“不准确”,我们可以解决这个问题,并且几十年前就会这样做。我们没有的原因是 FP 格式紧凑且快速,它是处理大量数字的最佳方式。此外,它是太空时代和军备竞赛的遗产,以及使用小型内存系统用非常慢的计算机解决大问题的早期尝试。(有时,单个磁芯用于 1 位存储,但那是另一回事了。)
结论
如果您只是在银行数豆子,那么首先使用十进制字符串表示的软件解决方案可以很好地运行。但是你不能用这种方式做量子色动力学或空气动力学。
评论
nextafter()
存储在计算机中的浮点数由两部分组成,一个整数和一个指数,取基数并乘以整数部分。
如果计算机以 10 为基数工作,则 、 和 。整数数学是简单而精确的,所以相加显然会得到 .0.1
1 x 10⁻¹
0.2
2 x 10⁻¹
0.3
3 x 10⁻¹
0.1 + 0.2
0.3
计算机通常不以 10 为基数工作,而是以 2 为基数工作。您仍然可以获得某些值的确切结果,例如 is 和 is ,将它们相加会导致 或 。完全。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.3000000000000000444089209850062616169452667236328125
评论
人们总是认为这是计算机的问题,但如果你用手数(以 10 为基数),除非你有无穷大来增加 0.333,否则你无法得到......至 0.333...因此,就像基数 2 中的问题一样,您将其截断为 0.333 + 0.333 = 0.666,并可能将其四舍五入为 0.667,这在技术上也是不准确的。(1/3+1/3=2/3)=true
(1/10+2/10)!==3/10
不过,用三元数,三分不是问题——也许有些每只手有 15 根手指的比赛会问为什么你的十进制数学被打破了......
评论
只是为了好玩,我按照标准 C99 的定义玩了浮点数的表示,并编写了下面的代码。
该代码在 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);
}
这是一个控制台会话,我在其中计算硬件中存在的浮点数的实际值。我曾经打印主程序输出的术语总和。也可以在 python 或类似的东西中插入该总和。bc
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
您还可以检查 -3.14 是否也受到干扰。不要忘记在 中设置一个因子。bc
scale
bc
显示的总和是硬件内部的总和。通过计算获得的值取决于设置的比例。我确实将系数设置为 15。从数学上讲,以无限的精度,它似乎是 1,000,000,000。scale
另一种看待方式:使用 64 位来表示数字。因此,无法精确表示超过 2**64 = 18,446,744,073,709,551,616 个不同的数字。
然而,Math 说 0 到 1 之间已经有无限多的小数点了。IEE 754 定义了一种编码,以有效地将这 64 位用于更大的数字空间以及 NaN 和 +/- 无穷大,因此准确表示的数字之间存在间隙,这些数字仅填充近似值。
不幸的是,0.3 位于缺口中。
有一些项目可以解决浮点实现问题。
以 Unum & Posit 为例,它展示了一种称为 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
您的预期结果是,在这种情况下,这意味着您需要一个精度为 1 位的结果。
所以你应该使用,但你不能只给 toFixed() 一个特定的参数,因为它取决于给定的数字,例如0.9
(0.2 + 0.7).tofixed(1)
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
仅供参考,乘法也存在同样的问题,例如返回.应用 floatify 函数作为解决方法:返回0.09 * 10
0.8999999999999999
floatify(0.09 * 10)
0.9
评论
0.1
从 Python 3.5 开始,你已经能够使用 math.isclose()
函数来测试近似相等性:
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
想象一下,以 8 位数的精度以 10 为基数工作。您检查是否
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.000110011001100100...(基数 2)
和
1/5 = 0.001100110011001001...(基数 2)
如果我们把它们截断为七位,那么我们就会得到
0.0001100 + 0.0011001 = 0.0100101
而另一方面,
3/10 = 0.01001100110011...(基数 2)
截断为七位,是 ,它们正好相差 。0.0100110
0.0000001
确切的情况稍微微妙一些,因为这些数字通常以科学记数法存储。因此,例如,我们可以将其存储为 ,而不是存储 1/10,具体取决于我们为指数和尾数分配了多少位。这会影响您获得的计算精度位数。0.0001100
1.10011 * 2^-4
结果是,由于这些舍入错误,您基本上永远不想在浮点数上使用 ==。相反,您可以检查它们差值的绝对值是否小于某个固定的小数。
十进制数(如 、 和)在二进制编码的浮点类型中不精确表示。和的近似之和与用于 的近似不同,因此 的谬误可以在这里更清楚地看到: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 标准没有指定此类类型,而是作为技术报告中描述的扩展。
和类型可能在您的系统上可用(例如,GCC 在选定的目标上支持它们,但 Clang 在 OS X 上不支持它们)。_Decimal32
_Decimal64
_Decimal128
这其实很简单。当你有一个以 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 上常用的格式,因为它比单精度浮点具有更宽的范围,尽管其性能和带宽成本很高。与单精度浮点格式一样,与相同大小的整数格式相比,它在整数上缺乏精度。它通常简称为双倍。IEEE 754 标准将 binary64 指定为具有:
- 符号位:1 位
- 指数:11 位
- 显著精度:53 位(显式存储 52 位)
给定的 64 位双精度基准面具有给定的偏置指数和 52 位分数所假定的实际值为
或
感谢@a_guest向我指出这一点。
浮点数在硬件级别表示为二进制数(以 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,因此舍入向下。
如果您的情况是将十进制数向下舍入一半很重要,则应使用 decimal 模块。顺便说一句,decimal 模块还提供了一种方便的方式来“查看”为任何浮点数存储的确切值。
>>> 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”的问题将在下面的“表示错误”一节中详细解释。请参阅 The Perils of Floating Point 以获取此类意外的更完整列表。
诚然,没有简单的答案,但是不要对浮动虚拟号码过分怀疑!在 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)
在:
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.10000000000000000005551115123125。在 Python 2.7 和 Python 3.1 之前的版本中,Python 将这些值四舍五入到 17 位有效小数位,显示“0.1000000000000000001”。在当前版本的 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 在将浮点数转换为“小数”时会精确转换。十进制”。这是我所知道的获得浮点数的精确十进制等效项的最简单方法。
在十进制中,1/3 不能用有限数量的数字精确表示。
例如,1/3 ~ 0.33333333 和 3 x (1/3) ~ 0.99999999 != 1。
同样,在二进制中,1/5 不能用有限位数的位数精确表示。
例如,1/5 ~ 0.00110011b 和 101b x 0.00110011b = 0.11111111 != 1。
简单地忽略所有细节,只考虑数字基础,如不同的几何形状——圆形、正方形、三角形、六边形、梯形(也是多维形状)等。
假设一种矩形的长宽比为 3:1,而正方形的长宽比为 3:1,您可以在该矩形中恰好容纳三个正方形。类比中的等价物是较大的形状(底数)是较小底数的整数幂,
喜欢与。
2
8 = 2 x 2 x 2
同样,其中三个矩形堆叠在一起,形成一个完美的正方形,其面积是原始正方形的九倍,您也可以通过伪数学来确认:
2 ^ 9 = 512 = 8 ^ 3
--- (同样,这是一种不同类型的非欧几里得类比,而不是正方形的
18 维物体^9)
两个矩形,一个水平比例为 19:13,另一个垂直比例为 7:13。两者都没有完美地映射到另一个,没有留下任何差距,但最终他们会相遇,因为它们在一侧共享相同的 13 个。
类比等价物类似于数值基数
216 = 6^3
与。7,776 = 6^5
-- 在 - 他们权力的 LCM 点开会
(6 ^ 3) ^ 5 = 470,184,984,576 = (6 ^ 5) ^ 3
- 一个圆与一个六边形相当于底数,比如说,61 对 181。它们可能会很接近,但由于它们根本没有共享任何素基,因此无论您有多少个六边形,您都无法将每个可能的圆都放入其中,反之亦然。
那么你的问题就相当于问:
为什么这个圆不能完全填满有限数量的梯形?
此答案旨在解决 JavaScript 问题,但它已被关闭、标记为重复并重定向到此处。但最有可能的是,我的答案适用于许多其他语言,因为使用 float 的逻辑在任何地方都大致相同。
不幸的是,没有优雅的方法可以在不遇到此类错误的情况下对浮点数执行算术运算。这是因为处理器不使用十进制数运行;他们使用二进制的。不同的处理器甚至可能以不同的方式工作,从而导致计算中的错误。若要查看数字的实际外观,可以运行以下代码:0.2
(0.2).toPrecision(50)
// 0.20000000000000001110223024625156540423631668090820
计算机的内存在物理上不可能存储确切的数字。JavaScript 的工作是找到一个非常接近的二进制数,后面有一定数量的零。0.2
0.2
同样,也没有优雅的方法来四舍五入这些数字!即使是标准的 JavaScript 函数(如 、 )也可能不准确。这可能因计算机而异,因为不同的处理器可能会产生不同的结果。toFixed
toPrecision
(8.005).toPrecision(3) // 8.01
(1.005).toPrecision(3) // 1.00
(8.005).toFixed(2) // 8.01
(1.005).toFixed(2) // 1.00
Math.round((8.005) * 100) / 100 // 8.01
Math.round((1.005) * 100) / 100 // 1.00, surprise!
// This is the reason:
(8.005) * 100 = 800.5000000000001
(1.005) * 100 = 100.49999999999999
在我看来,处理浮点数的最简单方法是经常对它们进行四舍五入,减少算术运算中累积的误差。这使我们能够继续使用标准数字类型,而不会使我们的程序复杂化。
我相信最可靠的四舍五入方法是将数字转换为字符串并手动按字符四舍五入。从字符串派生的数字将转换回相同的字符串形式,而不做任何更改。 这对于消除错误至关重要,特别是考虑到 JavaScript 代码中的所有数字常量都是文本,因为整个代码都是文本。JavaScript 的编译器将代码中的所有数字常量转换为数字,就像 .parseFloat('0.2').toString() === '0.2'
parseFloat
基于这些考虑,我编写了用于正确舍入的函数,基于与字符串的转换,并尽可能地优化性能。我的解决方案已经过不同数量的全面测试(~130,000 个测试用例)。
您可以在浏览器控制台中对此进行验证。如果您有更优雅或更高效的解决方案,请使用我的测试来测试它们。我很高兴看到这样的解决方案。
我的解决方案
const MAX_FLOAT_PRECISION = 15
const ROUND = 0
const FLOOR = 1
const CEIL = 2
const PRECISION = 0
const FRACTION = 1
function _round(value, digits, digitsType, roundType) {
if (digitsType === PRECISION && digits <= 0) {
throw new Error(`Precision digits (${digits}) must be > 0`)
}
if (digitsType === FRACTION && digits < 0) {
throw new Error(`Fraction digits (${digits}) must be >= 0`)
}
if (!value) {
return value
}
const negative = value < 0
const valueAbs = negative ? -value : value
const str = valueAbs.toExponential()
const len = str.indexOf('e')
let precisionDigits = digits
let exponent
if (digitsType === FRACTION) {
exponent = parseInt(str.slice(len + 1))
precisionDigits += exponent + 1
}
let index = precisionDigits > 0 ? precisionDigits + 1 : precisionDigits
if (index >= len) {
return value
}
let ch = index < 0 ? 0 : str.charCodeAt(index) - 48
let increment
switch (roundType) {
case ROUND:
increment = ch >= 5 ? (!negative ? true : index < len - 1) : false
break
case FLOOR:
increment = negative
break
case CEIL:
increment = !negative
break
}
if (precisionDigits <= 0) {
if (increment) {
if (typeof exponent === 'undefined') {
exponent = parseInt(str.slice(len + 1))
}
return parseFloat(
`${negative ? '-' : ''}1e${exponent - precisionDigits + 1}`,
)
}
return negative ? -0 : 0
}
if (!increment) {
return parseFloat(
(negative ? '-' : '') +
str.slice(0, index === 2 ? 1 : index) +
str.slice(len),
)
}
for (let nDigit = precisionDigits - 1; nDigit >= 0; nDigit--) {
index = nDigit > 0 ? nDigit + 1 : nDigit
ch = str.charCodeAt(index) - 48
if (ch < 9) {
return parseFloat(
(negative ? '-' : '') + str.slice(0, index) + (ch + 1) + str.slice(len),
)
}
}
return parseFloat((negative ? '-' : '') + '10' + str.slice(len))
}
function roundPrecision(value, digits) {
return _round(value, digits, PRECISION, ROUND)
}
function floorPrecision(value, digits) {
return _round(value, digits, PRECISION, FLOOR)
}
function ceilPrecision(value, digits) {
return _round(value, digits, PRECISION, CEIL)
}
function roundFraction(value, fractionDigits) {
return _round(value, fractionDigits || 0, FRACTION, ROUND)
}
function floorFraction(value, fractionDigits) {
return _round(value, fractionDigits || 0, FRACTION, FLOOR)
}
function ceilFraction(value, fractionDigits) {
return _round(value, fractionDigits || 0, FRACTION, CEIL)
}
/**
* @example 0.0000000000001 => 0
* @example 0.9999999999999 => 1
*/
function fixFloat(value) {
return roundPrecision(value, MAX_FLOAT_PRECISION)
}
使用示例
console.log(roundPrecision(100500, 3)); // 101000
console.log(roundPrecision(100499.9, 3)); // 100000
console.log(floorPrecision(100999.9, 3)); // 100000
console.log(ceilPrecision(100000.1, 3)); // 101000
console.log(roundFraction(100.005, 2)); // 100.01
console.log(roundFraction(100.00499, 2)); // 100
console.log(floorFraction(100.00999, 2)); // 100
console.log(ceilFraction(100.00001, 2)); // 100.001
console.log(fixFloat(0.1 + 0.2) === 0.3); // true
测试
function assert(actual, expected) {
if (actual !== expected) {
throw new Error(`Assertion failed: expected ${expected}, got ${actual}`)
}
}
function createValue({ lastDigits, countDigits, exponent }) {
if (countDigits === 0) {
lastDigits = 0
}
if (countDigits === 1) {
lastDigits = Math.round(lastDigits / 10) * 10
}
const digitsStr = lastDigits
.toString()
.padStart(Math.min(2, countDigits), '0')
.padStart(countDigits, '9')
const _exponent = exponent - countDigits
const value = parseFloat(`${digitsStr}e${_exponent}`)
return value
}
function test() {
const lastDigitsArray = Array.from({ length: 49 }, (_, i) => i)
const countDigitsArray = Array.from({ length: 14 }, (_, i) => i + 2)
const exponentArray = Array.from({ length: 200 }, (_, i) => i - 100)
lastDigitsArray.forEach(lastDigits => {
countDigitsArray.forEach(countDigits => {
exponentArray.forEach(exponent => {
const value = createValue({ lastDigits, countDigits, exponent })
assert(fixFloat(value), value)
// roundPrecision
assert(roundPrecision(value, countDigits), value)
assert(roundPrecision(value, countDigits + 1), value)
assert(
roundPrecision(value, countDigits - 1),
createValue({
lastDigits:
countDigits >= 3 || lastDigits >= 10
? Math.round(lastDigits / 10) * 10
: lastDigits,
countDigits,
exponent,
}),
)
// floorFraction
if (countDigits >= exponent) {
const fractionDigits = countDigits - exponent
assert(floorFraction(value, fractionDigits), value)
assert(floorFraction(value, fractionDigits + 1), value)
if (fractionDigits > 0) {
assert(
floorFraction(value, fractionDigits - 1),
createValue({
lastDigits: Math.floor(lastDigits / 10) * 10,
countDigits,
exponent,
}),
)
}
}
// ceilFraction
if (countDigits >= exponent) {
const fractionDigits = countDigits - exponent
assert(ceilFraction(value, fractionDigits), value)
assert(ceilFraction(value, fractionDigits + 1), value)
if (fractionDigits > 0) {
assert(
ceilFraction(value, fractionDigits - 1),
createValue({
lastDigits: Math.ceil(lastDigits / 10) * 10,
countDigits,
exponent,
}),
)
}
}
// floorPrecision
assert(floorPrecision(value, countDigits), value)
assert(floorPrecision(value, countDigits + 1), value)
assert(
floorPrecision(value, countDigits - 1),
createValue({
lastDigits:
countDigits >= 3 || lastDigits >= 10
? Math.floor(lastDigits / 10) * 10
: lastDigits,
countDigits,
exponent,
}),
)
// ceilPrecision
assert(ceilPrecision(value, countDigits), value)
assert(ceilPrecision(value, countDigits + 1), value)
assert(
ceilPrecision(value, countDigits - 1),
createValue({
lastDigits:
countDigits >= 3 || lastDigits >= 10
? Math.ceil(lastDigits / 10) * 10
: lastDigits,
countDigits,
exponent,
}),
)
})
})
})
console.log('TEST PASSED')
}
test()
评论