-
09-09-2019 - |
题
考虑以下代码:
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 十六进制浮点表示法.
相比之下,有理数 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
因此不同意代码中的常量。
对浮点算术问题的相当全面的处理是 每个计算机科学家都应该了解的浮点运算知识. 。有关更容易理解的解释,请参阅 浮点指南.de.
边注:所有位置(以 N 为基数)数字系统都精确地存在这个问题
普通的旧十进制(以 10 为基数)数字也有同样的问题,这就是为什么像 1/3 这样的数字最终会变成 0.333333333...
您刚刚偶然发现了一个数字 ( 3/10 ),它很容易用十进制表示,但不适合二进制系统。它也是双向的(在某种程度上):1/16 在十进制中是一个难看的数字 (0.0625),但在二进制中它看起来就像十进制中的万分之一 (0.0001)** - 如果我们习惯在日常生活中使用以 2 为基数的数字系统生活中,你甚至会看到这个数字,并本能地理解你可以通过将某物减半、再减半、一次又一次地达到这个目标。
** 当然,这并不完全是浮点数在内存中的存储方式(它们使用科学记数法的形式)。然而,它确实说明了二进制浮点精度误差往往会出现,因为我们通常感兴趣的“现实世界”数字通常是十的幂 - 但这只是因为我们使用十进制数字系统日 -今天。这也是为什么我们会说 71%,而不是“每 7 中就有 5”(71% 是一个近似值,因为 5/7 无法用任何十进制数字精确表示)。
所以不行:二进制浮点数并没有被破坏,它们只是碰巧和其他所有 N 基数系统一样不完美:)
侧边注:在编程中使用浮点数
实际上,这种精度问题意味着您需要使用舍入函数将浮点数舍入到您感兴趣的小数位数,然后再显示它们。
您还需要用允许一定程度容差的比较来替换相等测试,这意味着:
做 不是 做 if (float1 == float2) { ... }
相反做 if (Math.Abs(float1 - float2) < myToleranceValue) { ... }
.
需要根据您的特定应用程序选择 myToleranceValue - 这与您准备允许多少“摆动空间”以及您要比较的最大数字可能有很大关系(由于精度损失)问题)。请注意您选择的语言中的“double.Epsilon”样式常量(Javascript 中的 Number.EPSILON)。这些都是 不是 用作公差值。
有关公差的更多信息:
(编辑无耻的自我推销 - 对于劫持表示抱歉)
我在以下位置整理了有关如何选择容差以及为什么要避免 Number.EPSILON 及其同类的更详细说明: https://dev.to/alldanielscott/how-to-compare-numbers- Correctly-in-javascript-1l4i
其他提示
硬件设计师的视角
我相信我应该添加硬件设计师的视角,因为我设计和构建浮点硬件。了解错误的根源可能有助于理解软件中发生的情况,最终,我希望这有助于解释浮点错误发生并似乎随着时间的推移而累积的原因。
1.概述
从工程角度来看,大多数浮点运算都会有一些误差,因为进行浮点计算的硬件只需要在最后一位的误差小于一个单位的二分之一。因此,许多硬件将停止在一个精度,该精度仅需要在最后一位产生小于一个单位的二分之一的误差。 单次操作 这在浮点除法中尤其成问题。单个操作的构成取决于该单元需要多少个操作数。对于大多数来说,它是两个,但有些单元需要 3 个或更多操作数。因此,不能保证重复操作会导致所需的错误,因为错误会随着时间的推移而累积。
2.标准
大多数处理器遵循 IEEE-754 标准,但有些使用不可分割的标准或不同的标准。例如,IEEE-754 中有一种非规范化模式,它允许以牺牲精度为代价来表示非常小的浮点数。然而,下面将介绍 IEEE-754 的标准化模式,这是典型的操作模式。
在IEEE-754标准中,硬件设计者可以使用任何误差/epsilon值,只要它小于最后一位的一个单位的二分之一,并且结果只需小于最后一位的一个单位的二分之一即可。一次操作的地方。这就解释了为什么当重复操作时,错误会累积起来。对于 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,因为随着时间的推移,错误更有可能相互抵消,并且 区间算术 结合变化的 IEEE 754 舍入模式 预测舍入误差并纠正它们。由于与其他舍入模式相比相对误差较低,因此舍入到最接近的偶数位(最后一位)是 IEEE-754 的默认舍入模式。
请注意,默认舍入模式,舍入到最近的 最后一位的偶数, ,保证一次运算最后一位的误差小于二分之一。单独使用截断、向上取整、向下取整可能会导致误差大于最后一位的二分之一,但小于最后一位的一个单位,所以不建议使用这些模式,除非是用于区间算术。
6.概括
简而言之,浮点运算出错的根本原因是硬件截断和除法时倒数截断的结合。由于 IEEE-754 标准仅要求单次运算的最后一位的误差小于二分之一,因此重复运算的浮点误差将会累加,除非进行纠正。
当您转换0.1或1/10至基部2(二进制)你得到的小数点后的重复图案,就像试图代表1/3以10为底数的值不是精确的,并且因此可以“T做确切数学与它正常使用浮点方法。
这里的大多数答案都用非常枯燥的技术术语来解决这个问题。我想用普通人可以理解的方式来解决这个问题。
想象一下您正在尝试切披萨。你有一个可以切披萨片的机器人披萨切割机 确切地 一半。它可以将整个披萨减半,也可以将现有的披萨减半,但无论如何,减半总是准确的。
那个披萨刀的动作非常精细,如果你从整个披萨开始,然后将其减半,然后每次继续将最小的切片减半,你就可以完成减半 53次 在切片对于其高精度能力来说太小之前。那时,您不能再将那个非常薄的切片减半,而必须按原样包含或排除它。
现在,您如何将所有切片拼凑成披萨的十分之一 (0.1) 或五分之一 (0.2)?认真思考一下,并尝试解决它。如果您手边有一个神话般的精密披萨刀,您甚至可以尝试使用真正的披萨。:-)
当然,大多数有经验的程序员都知道真正的答案,那就是没有办法将一个 精确的 十分之一或五分之一的披萨使用这些切片,无论你把它们切片得有多细。你可以做一个相当好的近似,如果你将 0.1 的近似值与 0.2 的近似值相加,你会得到一个相当好的 0.3 的近似值,但它仍然只是一个近似值。
对于双精度数字(允许您将披萨减半 53 次的精度),立即小于和大于 0.1 的数字为 0.09999999999999999167332731531132594682276248931884765625 和 0.1000000000000000055511 151231257827021181583404541015625。后者比前者更接近 0.1,因此在给定输入 0.1 的情况下,数字解析器将倾向于后者。
(这两个数字之间的差异是我们必须决定包含(引入向上偏差)或排除(引入向下偏差)的“最小部分”。最小切片的技术术语是 乌尔普.)
在 0.2 的情况下,数字都是相同的,只是放大了 2 倍。同样,我们更喜欢略高于 0.2 的值。
请注意,在这两种情况下,0.1 和 0.2 的近似值都有轻微的向上偏差。如果我们添加足够多的这些偏差,它们将使数字越来越远离我们想要的,事实上,在 0.1 + 0.2 的情况下,偏差足够高,导致结果数字不再是最接近的数字至 0.3。
特别是,0.1 + 0.2 实际上是 0.1000000000000000055511151231257827021181583404541015625 + 0.2000000000000000111022302462515654042363166809 08203125 = 0.3000000000000000444089209850062616169452667236328125,而最接近0.3的数字实际上是0.29999999999999998889776975374843459576368331 9091796875。
附:一些编程语言还提供披萨切割器,可以 将切片分成精确的十分之一. 。尽管这种披萨刀并不常见,但如果您确实有的话,当需要精确切出十分之一或五分之一的披萨时,您应该使用它。
浮点舍入误差。 0.1不能在碱-2在碱-10精确地表示由于5.正如三分之一需要的位数的无限数量十进制来表示失踪素因子,但在基地-3“0.1”, 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
表达方式 0.1 + 0.2 === 0.3
回报 false
在 JavaScript 中,幸运的是浮点中的整数运算是精确的,因此可以通过缩放来避免小数表示错误。
作为一个实际示例,为了避免精度至关重要的浮点问题,建议1 将货币处理为代表美分数量的整数: 2550
美分而不是 25.50
美元。
1 道格拉斯·克罗克福德: JavaScript:好的部分: :附录 A - 糟糕的零件(第 105 页).
我的回答很长,所以我把它分成三个部分。由于问题是关于浮点数学的,因此我将重点放在机器实际执行的操作上。我还使其特定于双精度(64 位),但该参数同样适用于任何浮点算术。
前言
一个 IEEE 754 双精度二进制浮点格式(binary64) number 代表表格中的数字
值 = (-1)^s * (1.m51米50...米2米1米0)2 * 2e-1023
64 位:
- 第一个位是 符号位:
1
如果数字为负数,0
否则1. - 接下来的 11 位是 指数, ,即 抵消 到 1023。换句话说,从双精度数读取指数位后,必须减去 1023 才能获得 2 的幂。
- 剩下的 52 位是 有效数 (或尾数)。在尾数中,“隐含”
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).
将双精度数转换为二进制数
许多在线转换器可将双精度浮点数转换为二进制(例如在 二进制转换网站),但这里有一些示例 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 * 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} 我们将指数加一并移动小数位 (二进制) 点得到:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
= 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875
尾数现在有 53 位(第 53 位位于上行的方括号中)。默认 舍入模式 对于 IEEE 754 来说是 '舍入到最接近的值' - IE。如果一个数字 X 落在两个值之间 A 和 乙, ,选择最低有效位为零的值。
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 和 乙 仅最后一位不同; ...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 允许的数字表示形式。由于默认舍入模式,这些表示的添加会产生仅在最低有效位上不同的值。
长话短说
写作 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
是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
的精确十进制值。浮点数通常为圆形以用于显示。
浮点舍入误差。从什么每台计算机科学家应该知道关于浮点运算:
挤压无限多的实数成有限数目的比特需要的近似表示。虽然有无限多的整数,在大多数程序整数计算的结果可被存储在32位。相比之下,给定任意固定位数,大多数计算实数会产生一种不能使用,很多位准确表示数量。因此浮点计算的结果常常必须以适应回其有限表示进行四舍五入。此舍入误差是浮点计算的特征。
我的解决方法:
function add(a, b, precision) {
var x = Math.pow(10, precision || 2);
return (Math.round(a * x) + Math.round(b * x)) / x;
}
精度的引用要在添加过程中小数点后保留的位数。
与此相关的著名的双精度问题的一些统计数据。
当加入所有的值( A + B )使用0.1(0.1至100)中的步骤,我们有精度误差的〜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)
当减去所有的值(一 - B 其中 A> B )使用0.1(100〜0.1)的工序,我们有的〜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%)。
否,不易破碎,但大多数小数必须近似
摘要的
浮点运算的是的确切的,不幸的是,它不与我们平时的基础-10号表示配合地很好,所以原来我们经常给它输入的是稍微偏离从什么我们写。
即使是简单的号码,如0.01%,0.02,0.03,0.04 0.24 ...不完全表示为二进制小数。如果算上上涨0.01,0.02,0.03 ...,直到你到0.25将你在库中的第一部分表示的 2 。如果你尝试过,使用FP,你的0.01本来稍微偏离,所以唯一的办法,以增加他们的25到一个不错的确切0.25将需要的因果关系,涉及保护位和舍入的长链。这是很难预测的,所以我们丢了我们的手,说的“FP是不准确的”,的,但是这不是真的。
我们不断得到FP硬件看起来好象在基座10简单,但是在基座2的重复部分。
这是怎么回事?的
当我们以十进制,每种级分写(具体地,每终止十进制)的形式为一有理数
A /(2 名词×5 米)
在二进制中,我们只获得 2 名词 的术语,即:
A / 2 名词
因此,在十进制中,我们不能代表 1 / <子> 3 子>。因为基座10包括2作为主要因素,每一个数字,我们可以作为二进制分数的也可以写为一个基座10分数写。然而,几乎没有任何我们写出作为碱<子> 10 子>分数在二进制表示的。范围为0.01%,0.02,0.03 0.99 ...,只 3 号可以在我们FP格式表示:0.25,0.50,和0.75,因为它们是1/4,1/2,和3/4,仅使用2 名词术语与素数因子的所有数字。
在碱<子> 10 子>我们不能代表 1 / <子> 3 子>。但是,在二进制的,我们不能做 1 / <子> 10 子> 或 1 / <子> 3 子>
因此,虽然每一个二进制分数可以用十进制被写入时,反向是不正确的。而实际上大多数小数重复二进制。
处理它
开发通常指示做的 <小量的比较,更好的建议可能是四舍五入到整数值(在C库:ROUND()和roundf(),即留在FP格式),然后进行比较。舍入到一个特定的小数长度解决了与输出大多数问题。
此外,在真实的数字运算的问题(即FP的发明为在早期的问题,可怕昂贵的计算机)宇宙的物理常数和所有其他测量仅已知相对较小数目的显著数字,所以整个问题空间是“不准确的”反正。 FP“准确性”是不是在这种应用中的一个问题。
在整个事件真的发生时,人们尝试用FP的豆计数。它不为工作,但只有当你坚持到整数值,哪一种违背了使用它的点。 这就是为什么我们有所有这些小数软件库。的
我爱由克里斯比萨答案,因为它描述了实际的问题,不只是一般的handwaving关于“不准确”。如果FP是简单的“不准确的”,我们可以的修复的这一点,本来几十年前做的。我们没有的原因是因为FP格式是紧凑和快速,这是紧缩很多数字的最好方式。此外,它是从太空时代化和军备竞赛的传统和早期试图解决使用小内存系统很慢的电脑大问题。 (有时,个别的磁芯 1位存储,但是这另一回事。)
结论
如果你只是在银行计数豆,在第一工作场所使用十进制字符串表示得很清楚的软件解决方案。但你不能做量子色或空气动力学的方式。
您是否尝试过的胶带解决方案?
尝试确定错误发生时和短if语句解决这些问题,这是不漂亮,但对于一些问题它是唯一的解决办法,这是其中之一。
if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
else { return n * 0.1 + 0.000000000000001 ;}
我在C#中的科学模拟项目同样的问题,我可以告诉你,如果你忽略了蝴蝶效应是会轮到大胖龙咬你在一个**
这些奇异数出现,因为计算机使用二进制(基数为2)的数字系统进行计算的目的,而我们使用十进制(基体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).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
不会在所有的工作虽然它看起来都是一样的!
我更喜欢第一解决方案,因为我能将它作为转换输入浮子精确的输出浮动的功能。
鉴于没有人提到这一点...
一些高级语言(例如 Python 和 Java)附带了克服二进制浮点限制的工具。例如:
蟒蛇的
decimal
模块 和Java的BigDecimal
班级, ,在内部用十进制表示法(而不是二进制表示法)表示数字。两者的精度都有限,因此仍然容易出错,但它们解决了二进制浮点运算的大多数常见问题。在处理金钱时,小数非常有用:十美分加二十美分总是正好是三十美分:
>>> 0.1 + 0.2 == 0.3 False >>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3') True
蟒蛇的
decimal
模块基于 IEEE 标准 854-1987.蟒蛇的
fractions
模块 和 Apache Common 的BigFraction
班级. 。两者都将有理数表示为(numerator, denominator)
对,它们可以给出比十进制浮点运算更准确的结果。
这些解决方案都不是完美的(特别是如果我们考虑性能,或者如果我们需要非常高的精度),但它们仍然解决了二进制浮点运算的大量问题。
很多这个问题的无数重复的询问有关浮点舍入对具体数字的影响。在实践中,这是比较容易得到它是如何工作通过查看刚刚阅读关于它的利益,而不是计算的精确结果的感觉。一些语言提供了这样做的方式 - 例如转换float
或double
在Java BigDecimal
由于这是一个语言无关的问题,它需要语言无关的工具,如一个十进制到浮点转换器。
将其应用到在问题的数字,作为双打处理:
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,在偶数位从而结束,因此是正确的结果。
我可以只添加;人们总是认为这是一个电脑问题,但如果你用你的手(基数为10)算,除非你有无限增加0.333 ... 0.333 ...所以就像用(1/3+1/3=2/3)=true
问题,你不能让(1/10+2/10)!==3/10
在基体2,可以将其截断为0.333 + 0.333 = 0.666,它可能四舍五入到0.667,其也将是在技术上是不准确的。
计数三元,而三分之二是不是一个问题 - 也许有些比赛用每只手15周的手指会问,为什么你的十进制数学被打破了......
那种浮点数学的,可以在数字式计算机中实现一定使用在其上的实数和操作的近似值。 (中的标准的版本运行到超过五十页的文件,并有一个委员会来处理它的勘误和进一步完善。)
这近似是不同种类的,其中的每一个可以被忽略或仔细占由于从精密偏差的其具体方式的近似值的混合物。这也涉及到在大多数人走过去的权利而假装没有注意到的硬件和软件水平都了一些明确的例外情况。
如果您需要无限精度(使用数字π,例如,而不是它的许多较短的替身之一),你应该写或使用符号数学程序代替。
但是,如果你没事的想法,有时浮点数学是价值逻辑和错误模糊可以迅速积累,你可以写你的要求和试验,以允许这一点,那么你的代码可以经常获得通过有什么在你的FPU。
只是为了好玩,我玩用浮漂的表示,下面的从标准C99的定义和我写下面的代码。
在代码打印浮子的二进制表示在3个分离组
SIGN EXPONENT FRACTION
和它打印的总和之后,即,当有足够的精确度相加,这将表明,确实存在于硬件的值。
因此,当你写float x = 999...
,编译器将变换数在由函数xx
这样打印的位表示由函数yy
印刷之和是等于给定的数。
在现实中,这总和只是一个近似值。对于编号999999999编译器将在所述浮子的位表示插入数十亿
代码后附上控制台会话,其中我计算两个常数术语(减去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还扰动。不要忘记在scale
设置bc
因素。
所显示的总和是什么硬件内。您可以通过计算其获得的值取决于您设置的规模。我没有设置scale
因子〜15在数学上,以无限的精度,似乎它是10亿。
另一种方式来看待这个:用于为64位来表示数字。作为结果,没有办法比2 ** 64 = 18,446,744,073,709,551,616不同数量可以精确地表示更多。
然而,数学说,已经有无限多的小数0和1之间IEE 754限定的编码以有效地使用这些64位为数量大得多的空间,加上为NaN和+/-无穷远,所以有精确表示之间的间隙写满数字的数字仅近似。
不幸的是0.3坐落在一个间隙。
由于这个线程分支比特到过电流浮点实现一般的讨论中,我想补充一点,有项目上固定他们的问题。
看一看 https://posithub.org/ 例如,展示称为POSIT号码类型(及其前身UNUM),承诺提供更好的精度与更少的比特。如果我的理解是正确的,同时也修复了同类产品中的问题问题。很有趣的项目,它背后的人是一位数学家它博士约翰·古斯塔夫森的。整个事情是开源的,在C / C ++,Python和朱莉娅和C#( HTTPS许多实际的实现:// hastlayer。 COM /算术的)。
试想在基座10与工作,比方说,精确度的8位数字。您检查是否
1/3 + 2 / 3 == 1
和学习,这返回false
。为什么?那么,作为实数,我们有
1/3 = 0.333 .... 和 2/3 = 0.666 ....
在小数点后8个位数截断,我们得到
0.33333333 + 0.66666666 = 0.99999999
其是,当然,从1.00000000
不同恰好0.00000001
。
具有固定数量的比特为二进制数的情况是完全类似。作为实数,我们有
1/10 = 0.0001100110011001100 ...(基数为2)
和
1/5 = 0.0011001100110011001 ...(基数为2)
如果我们截断这些,比方说,七位,然后我们会得到
0.0001100 + 0.0011001 = 0.0100101
,而在另一方面,
3/10 = 0.01001100110011 ...(基数为2)
其中,截断为七个位,是0.0100110
,并且这些恰好0.0000001
不同。
的确切情况稍微更微妙的,因为这些数字通常存储在科学记数法。所以,举例来说,而不是存储1/10 0.0001100
我们可以将它,存储为像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
<强> 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;
}
},
diff:{
value: function(A,B){
var prec = this.max(this.get_precision(A),this.get_precision(B));
return +this.precision(A-B,prec);
}
},
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.diff(0.2, 0.11) == 0.09 // true
0.2 - 0.11 == 0.09 // false
还注意到,Math.diff和Math.sum自动检测精度使用
Math.sum接受任何数量的参数
这其实很简单。当你有一个基地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号。
一个不同的问题已被命名为重复此之一:
在C ++中,这是为什么cout << x
从值不同,一个调试器表示用于x
?结果
在问题中的x
是float
变量。
一个示例将是
float x = 9.9F;
在调试器显示9.89999962
,cout
操作的输出是9.9
。
答案原来是为cout
该float
的默认精度为6,所以四舍五入到小数点后6位数字。
请参阅此处参考
这实际上是为了回答 这个问题 -- 作为重复项而关闭 这 问题, 尽管 我正在整理这个答案,所以现在我无法将其发布在那里......所以我会在这里发帖!
问题摘要:
在工作表上
10^-8/1000
和10^-11
评估为 平等的 而在 VBA 中则不然。
在工作表上,数字默认为科学记数法。
如果将单元格更改为数字格式 (控制键+1) 的 Number
和 15
小数点后,你得到:
=10^-11 returns 0.000000000010000
=10^(-8/1000) returns 0.981747943019984
所以说,他们绝对不是同一个人……一个约为 0,另一个约为 1。
Excel 的设计初衷并不是为了处理 极其 数量很少 - 至少在库存安装中不是这样。有一些插件可以帮助提高数字精度。
Excel 是根据 IEEE 二进制浮点运算标准 (IEEE 754)。该标准定义了如何 浮点数字 被存储和计算。这 IEEE 754 标准被广泛使用,因为它允许将浮点数存储在合理的空间中,并且计算可以相对快速地进行。
浮动相对于定点表示的优点是它可以支持更广泛的值。例如,具有 5 位小数且小数点位于第三位之后的定点表示可以表示数字
123.34
,12.23
,2.45
, , ETC。而 5 位精度的浮点表示可以表示 1.2345、12345、0.00012345 等。同样,浮点表示也允许在保持精度的同时进行大范围的计算。例如,
其他参考资料:
- 办公室支持: 以科学(指数)表示法显示数字
- Microsoft 365 博客: 了解浮点精度, ,又名 “为什么 Excel 给出的答案看似错误?”
- 办公室支持: 在 Excel 中设置舍入精度
- 办公室支持:
POWER
功能 - 超级用户: 我可以在 Excel VBA 变量中存储的最大值(数字)是多少?
小数分数例如 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 上不支持它们)。