浮点数学被破坏了吗?

问:

考虑以下代码:

0.1 + 0.2 == 0.3  ->  false

0.1 + 0.2         ->  0.30000000000000004

为什么会出现这些错误?

答1:

huntsbot.com聚合了超过10+全球外包任务平台的外包需求,寻找外包任务与机会变的简单与高效。

二进制 floating point 数学是这样的。在大多数编程语言中,它基于 IEEE 754 standard。问题的症结在于,数字以这种格式表示为整数乘以 2 的幂。分母不是 2 的幂的有理数(如 0.1,即 1/10)无法精确表示。

对于标准 binary64 格式的 0.1,表示可以完全写为

0.10000000000000000055511151231257827021181583404541015625(十进制),或

0x1.999999999999ap-4 C99 hexfloat 表示法。

相反,有理数 0.1,即 1/10,可以完全写为

0.1(十进制),或

0x1.99999999999999…p-4 类似于 C99 hexfloat 表示法,其中 … 表示 9 的无休止序列。

程序中的常量 0.2 和 0.3 也将近似于它们的真实值。碰巧最接近 0.2 的 double 大于有理数 0.2,但最接近 double 的 0.3 小于有理数 0.3。 0.1 和 0.2 的总和最终大于有理数 0.3,因此与代码中的常数不一致。

浮点算术问题的一个相当全面的处理是What Every Computer Scientist Should Know About Floating-Point Arithmetic。有关更易于理解的解释,请参见floating-point-gui.de。

旁注:所有位置(base-N)数字系统都精确地共享这个问题

普通的旧十进制(以 10 为底)数字也有同样的问题,这就是为什么像 1/3 这样的数字最终会变成 0.333333333…

您刚刚偶然发现了一个数字 (3/10),它恰好很容易用十进制系统表示,但不适合二进制系统。它也是双向的(在某种程度上):1/16 是十进制的丑数(0.0625),但在二进制中它看起来就像十进制中的万分之一一样整洁(0.0001)** - 如果我们在在我们的日常生活中使用以 2 为底的数字系统的习惯,你甚至会看到这个数字并本能地理解你可以通过减半、一次又一次、一次又一次地减半来到达那里。

** 当然,浮点数在内存中的存储方式并不完全正确(它们使用一种科学记数法)。然而,它确实说明了二进制浮点精度错误往往会突然出现的一点,因为我们通常感兴趣的“现实世界”数字通常是十的幂 - 但仅仅是因为我们使用十进制数字系统 -今天。这也是为什么我们会说 71% 而不是“每 7 个中有 5 个”之类的东西(71% 是一个近似值,因为 5/7 不能用任何十进制数精确表示)。

所以不:二进制浮点数没有被破坏,它们只是碰巧和其他所有基于 N 的数字系统一样不完美:)

旁注:在编程中使用浮点数

在实践中,这个精度问题意味着您需要使用舍入函数将浮点数四舍五入到您感兴趣的小数位,然后再显示它们。

您还需要用允许一定容差的比较替换相等测试,这意味着:

不做if (x == y) { … }

而是执行 if (abs(x - y) < myToleranceValue) { … }。

其中 abs 是绝对值。 myToleranceValue 需要为您的特定应用程序选择 - 这与您准备允许多少“摆动空间”以及您要比较的最大数字可能是多少(由于丢失精度问题)。请注意您选择的语言中的“epsilon”样式常量。这些不可用作公差值。

我认为“某个错误常数”比“The Epsilon”更正确,因为没有可以在所有情况下使用的“The Epsilon”。在不同的情况下需要使用不同的 epsilon。并且机器 epsilon 几乎从来都不是一个好用的常数。

并不是所有的浮点数学都基于 IEEE [754] 标准。例如,仍然有一些系统使用旧的 IBM 十六进制 FP,并且仍然有不支持 IEEE-754 算法的显卡。然而,这是一个合理的近似值。

为了速度,Cray 放弃了 IEEE-754 合规性。 Java 也放松了对优化的坚持。

我认为你应该在这个答案中添加一些关于货币计算应该如何始终使用整数的定点算术来完成的内容,因为货币是量化的。 (以美分的一小部分或任何最小的货币单位进行内部会计计算可能是有意义的——这通常有助于减少将“每月 29.99 美元”转换为每日汇率时的舍入误差——但它应该仍然是定点算术。)

有趣的事实:这个 0.1 在二进制浮点中没有精确表示,导致了一个臭名昭著的 Patriot missile software bug,在第一次伊拉克战争中导致 28 人丧生。

答2:

huntsbot.com – 程序员副业首选,一站式外包任务、远程工作、创意产品分享订阅平台。

硬件设计师的观点

我相信我应该为此添加硬件设计师的观点,因为我设计和构建浮点硬件。了解错误的来源可能有助于理解软件中发生的事情,最终,我希望这有助于解释浮点错误发生的原因,并且似乎随着时间的推移而累积。

一、概述

从工程的角度来看,大多数浮点运算都会有一些错误,因为进行浮点计算的硬件只需要在最后一个单位的误差小于一半。因此,许多硬件将停止在一个精度上,该精度只需要在单个操作的最后一个位置产生小于一半的误差,这在浮点除法中尤其成问题。什么构成单个操作取决于该单元需要多少个操作数。大多数情况下,它是两个,但有些单元需要 3 个或更多操作数。因此,不能保证重复的操作会导致理想的错误,因为错误会随着时间的推移而累积。

  1. 标准

大多数处理器遵循 IEEE-754 标准,但有些使用非规范化或不同的标准。例如,IEEE-754 中有一种非规范化模式,它允许以牺牲精度为代价来表示非常小的浮点数。然而,下面将介绍 IEEE-754 的标准化模式,这是典型的操作模式。

在 IEEE-754 标准中,允许硬件设计人员使用任何 error/epsilon 值,只要它小于最后一个单位的二分之一,并且结果只需小于最后一个单位的二分之一一个操作的地方。这就解释了为什么当有重复操作时,错误会加起来。对于 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 除法舍入误差:倒数的近似

商选择表中的倒数取决于division method:慢除法如 SRT 除法,或快速除法如 Goldschmidt 除法;每个条目都根据除法算法进行修改,以尝试产生尽可能低的错误。但是,无论如何,所有倒数都是实际倒数的近似值,并引入了一些误差因素。慢除法和快除法都迭代计算商,即每一步计算商的一些位数,然后从被除数中减去结果,除法器重复这些步骤,直到误差小于一的二分之一单位排在最后。慢除法方法在每个步骤中计算商的固定位数并且通常构建成本较低,而快速除法方法计算每个步骤的可变位数并且通常构建成本更高。除法方法最重要的部分是它们中的大多数依赖于重复乘以一个倒数的近似,因此它们容易出错。

  1. 其他运算中的舍入误差:截断

所有操作中舍入误差的另一个原因是 IEEE-754 允许的最终答案截断的不同模式。有截断、向零舍入、round-to-nearest (default), 向下舍入和向上舍入。所有方法都会在单个操作的最后一个位置引入小于一个单位的误差元素。随着时间的推移和重复的操作,截断也会累积地增加结果错误。这种截断误差在求幂中尤其成问题,它涉及某种形式的重复乘法。

5.重复操作

由于进行浮点计算的硬件只需要在单个操作的最后一个位置产生一个误差小于一半的结果,如果不注意,误差会随着重复操作而增长。这就是在需要有界误差的计算中,数学家使用诸如使用 IEEE-754 的四舍五入 even digit in the last place 等方法的原因,因为随着时间的推移,误差更有可能相互抵消,并且Interval Arithmetic 与 IEEE 754 rounding modes 的变体相结合,以预测舍入误差并进行纠正。由于与其他舍入模式相比,它的相对误差较低,舍入到最接近的偶数(在最后一位)是 IEEE-754 的默认舍入模式。

请注意,默认舍入模式,round-to-nearest even digit in the last place,保证一次操作的最后一位的误差小于一个单位的二分之一。单独使用截断、向上舍入和向下舍入可能会导致错误大于最后一位单位的二分之一,但小于最后一位单位,因此不建议使用这些模式,除非它们是用于区间算术。

6.总结

简而言之,浮点运算出错的根本原因是硬件截断和除法倒数截断的结合。由于 IEEE-754 标准只要求单次运算的最后一位误差小于一个单位的二分之一,因此重复运算的浮点误差会累加起来,除非得到纠正。

(3) 错误。一个分部的舍入误差不小于倒数一个单位,最多倒数半个单位。

@gnasher729 好收获。使用默认的 IEEE 舍入模式,大多数基本操作在最后一位的误差也小于一个单位的 1/2。编辑了解释,并且还注意到如果用户覆盖默认舍入模式(在嵌入式系统中尤其如此),错误可能大于 1 ulp 的 1/2 但小于 1 ulp。

(1) 浮点数没有错误。每个浮点值都是它的本来面目。大多数(但不是全部)浮点运算给出不精确的结果。例如,不存在完全等于 1.0/10.0 的二进制浮点值。另一方面,一些操作(例如,1.0 + 1.0)确实给出了准确的结果。

“浮点除法错误的主要原因,是用于计算商的除法算法”是一种非常具有误导性的说法。对于符合 IEEE-754 的除法,浮点除法错误的唯一原因是结果无法以结果格式精确表示;无论使用何种算法,都会计算出相同的结果。

@Matt 抱歉回复晚了。这基本上是由于资源/时间问题和权衡。有一种方法可以进行长除法/更“正常”的除法,它称为 SRT 除法,基数为 2。但是,这会重复移动并从被除数中减去除数,并且需要许多时钟周期,因为它只计算每个时钟周期的商的一位。我们使用倒数表,以便我们可以计算每个周期的商的更多位,并进行有效的性能/速度权衡。

答3:

与HuntsBot一起,探索全球自由职业机会–huntsbot.com

它的破坏方式与您在小学学习并每天使用的十进制(以 10 为底)符号完全相同,仅用于以 2 为底。

要理解,请考虑将 1/3 表示为十进制值。不可能完全做到!世界将在你写完小数点后的 3 之前结束,因此我们写了一些地方并认为它足够准确。

同样,1/10(十进制 0.1)不能以 2 进制(二进制)精确表示为“十进制”值;小数点后的重复模式永远持续下去。该值不准确,因此您无法使用普通浮点方法对其进行精确数学运算。就像以 10 为底的情况一样,还有其他值也表现出这个问题。

伟大而简短的答案。重复模式看起来像 0.00011001100110011001100110011001100110011001100110011 ...

有一些方法可以产生精确的十进制值。 BCD(二进制编码十进制)或各种其他形式的十进制数。然而,这些都比使用二进制浮点更慢(慢很多)并且占用更多的存储空间。 (例如,打包的 BCD 在一个字节中存储 2 个十进制数字。也就是说,一个字节中有 100 个可能的值,实际上可以存储 256 个可能的值,即 100/256,这浪费了大约 60% 的字节可能值。)

@IInspectable,对于浮点运算,基于 BCD 的数学运算比本机二进制浮点慢数百倍。

@DuncanC嗯,有些方法可以产生精确的十进制值——用于加法和减法。对于除法、乘法等,它们与二进制方法具有相同的问题。这就是为什么在会计中使用 BCD 的原因,因为它主要处理加号和减号,你不能解释任何小于一美分的东西。然而,像 1/3*3 == 1 这样简单的东西在 BCD 数学中失败(评估为假),就像在纸上使用十进制除法一样。

@DuncanC:“BCD 比二进制浮点慢很多,句号。” - 嗯,是的。除非不是。很确定有 architectures,其中 BCD 数学至少与 IEEE-754 浮点数学一样快(或更快)。但这不是重点:如果您需要小数精度,则不能使用 IEEE-754 浮点表示。这样做只会实现一件事:更快地计算错误的结果。

答4:

huntsbot.com全球7大洲远程工作机会,探索不一样的工作方式

这里的大多数答案都用非常枯燥的技术术语来解决这个问题。我想用普通人可以理解的方式来解决这个问题。

想象一下,您正在尝试切比萨饼。你有一个机器人披萨切割机,可以将披萨片精确地切成两半。它可以将整个披萨减半,也可以将现有切片减半,但无论如何,减半总是准确的。

那个比萨刀的动作非常精细,如果你从一整块比萨开始,然后把它减半,然后每次将最小的切片继续减半,你可以减半 53 次,直到切片太小,甚至无法达到高精度的能力.那时,您不能再将那个非常薄的切片减半,而必须按原样包含或排除它。

现在,您将如何将所有切片以这样一种方式拼凑起来,加起来相当于披萨的十分之一 (0.1) 或五分之一 (0.2)?认真想想,然后努力解决。如果您手头有一个神话般的精密比萨刀,您甚至可以尝试使用真正的比萨饼。 😃

当然,大多数有经验的程序员都知道真正的答案,那就是无论你把它们切成多细,都无法用这些切片将披萨的十分之一或五分之一拼凑起来。你可以做一个很好的近似,如果你将 0.1 的近似值与 0.2 的近似值相加,你会得到一个很好的近似值 0.3,但它仍然只是一个近似值。

对于双精度数字(这是使您可以将披萨减半53倍的精度),该数字立即少于0.1,是0.099999999999999999999999999999167327315315313259468227272727248931893155555555555555555555555555555555555555555555太平洋。后者比前者更接近 0.1,因此数字解析器将在输入 0.1 的情况下支持后者。

(这两个数字之间的差异是我们必须决定包含的“最小切片”,它会引入向上偏差,或者排除,它会引入向下偏差。最小切片的技术术语是 ulp。)

在 0.2 的情况下,数字都是相同的,只是放大了 2 倍。同样,我们支持略高于 0.2 的值。

请注意,在这两种情况下,0.1 和 0.2 的近似值都有轻微的向上偏差。如果我们添加足够多的这些偏差,它们会使数字越来越远离我们想要的,事实上,在 0.1 + 0.2 的情况下,偏差足够高,结果数字不再是最接近的数字为 0.3。

In particular, 0.1 + 0.2 is really 0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125, whereas the number closest to 0.3 is actually 0.299999999999999988897769753748434595763683319091796875.

PS 一些编程语言还提供可以 split slices into exact tenths 的比萨刀。虽然这样的比萨刀并不常见,但如果您确实可以使用它,您应该在重要的是能够准确地获得十分之一或五分之一的切片时使用它。

(Originally posted on Quora.)

请注意,有些语言包含精确的数学。一个例子是 Scheme,例如通过 GNU Guile。请参阅 draketo.de/english/exact-math-to-the-rescue — 这些将数学保留为分数,最后只进行切片。

@FloatingRock 实际上,很少有主流编程语言内置有理数。 Arne 和我一样是一名计划者,所以这些都是我们被宠坏的东西。

@ArneBabenhauserheide 我认为值得补充的是,这只适用于有理数。因此,如果您使用 pi 等无理数进行数学运算,则必须将其存储为 pi 的倍数。当然,任何涉及 pi 的计算都不能表示为精确的十进制数。

@connexo 好的。您将如何对您的披萨旋转器进行编程以获得 36 度?什么是36度? (提示:如果你能以精确的方式定义它,你也有一个精确的十分之一的比萨刀。)换句话说,你实际上不可能有 1/360(度数)或 1/ 10(36 度),只有二进制浮点。

@connexo 另外,“每个白痴”都不能将披萨正好旋转 36 度。人类太容易出错,无法做任何如此精确的事情。

答5:

打造属于自己的副业,开启自由职业之旅,从huntsbot.com开始!

浮点舍入错误。由于缺少 5 的素因子,0.1 在 base-2 中无法像在 base-10 中那样准确地表示。就像 1/3 需要无限位数以十进制表示,但在 base-3 中是“0.1”, 0.1 在 base-2 中采用无限位数,而在 base-10 中则没有。并且计算机没有无限量的内存。

@Pacerier 当然,他们可以使用两个无界精度整数来表示一个分数,或者他们可以使用引号表示法。正是“二进制”或“十进制”的特定概念使这变得不可能——你有一个二进制/十进制数字序列,并且在其中的某个地方有一个小数点。为了获得精确的理性结果,我们需要更好的格式。

@Pacerier:二进制和十进制浮点都不能精确存储 1/3 或 1/13。十进制浮点类型可以精确地表示 M/10^E 形式的值,但在表示大多数其他分数时不如类似大小的二进制浮点数精确。在许多应用程序中,使用任意分数获得更高的精度比使用一些“特殊”分数获得完美的精度更有用。

@supercat 在比较 binary64 和 decimal64 的精度时:精度相当 - 当然彼此相差 10 倍。授予十进制64 比二进制64 摆动更多。

@chux:二进制和十进制类型之间的精度差异并不大,但是十进制类型的最佳情况与最坏情况精度的 10:1 差异远大于二进制类型的 2:1 差异。我很好奇是否有人构建了硬件或编写了软件来有效地在任何一种十进制类型上运行,因为两者似乎都不适合在硬件或软件中有效实现。

@DevinJeanpierre我认为关键是“计算机”没有“'二进制'或'十进制'的特定概念”。 Pacerier 的观点似乎是语言设计者决定过早地跳转到“浮点”,在存储“0.1”、“0.2”和“0.3”这样的数字时,不仅可以更准确,而且也更节省空间地存储为文本 (BCD)。

答6:

打造属于自己的副业,开启自由职业之旅,从huntsbot.com开始!

我的答案很长,所以我把它分成三个部分。由于问题是关于浮点数学的,所以我把重点放在了机器的实际作用上。我还专门针对双精度(64 位)精度,但该参数同样适用于任何浮点运算。

前言

IEEE 754 double-precision binary floating-point format (binary64) 数字表示形式的数字

值 = (-1)^s * (1.m51m50…m2m1m0)2 * 2e-1023

64位:

第一位是符号位:如果数字为负,则为 1,否则为 0。

接下来的 11 位是指数,偏移了 1023。换句话说,从双精度数中读取指数位后,必须减去 1023 才能获得 2 的幂。

剩下的 52 位是有效数(或尾数)。在尾数中,“隐含”的 1. 总是被省略,因为任何二进制值的最高有效位都是 1。

1 - IEEE 754 允许 signed zero 的概念 - +0 和 -0 的处理方式不同:1 / (+0) 是正无穷大; 1 / (-0) 是负无穷大。对于零值,尾数和指数位都为零。注意:零值(+0 和 -0)明确不归类为非正规2。

2 - denormal numbers 不是这种情况,它的偏移指数为零(以及隐含的 0.)。非正规双精度数的范围是 dmin ≤ |x| ≤ dmax,其中 dmin(可表示的最小非零数)为 2-1023 - 51 (≈ 4.94 * 10- 324) 和 dmax(最大的非正规数,其尾数完全由 1s 组成)为 2-1023 + 1 - 2< sup>-1023 - 51 (≈ 2.225 * 10-308)。

将双精度数转换为二进制

存在许多在线转换器将双精度浮点数转换为二进制数(例如 binaryconvert.com),但这里有一些示例 C# 代码来获得双精度数的 IEEE 754 表示(我用冒号分隔三个部分({ 2}):

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 * 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} 的形式,我们将指数加一并移动小数点(二进制)得到:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)
    = 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875

现在尾数中有 53 位(第 53 位在上一行的方括号中)。 IEEE 754 的默认 rounding mode 是“四舍五入” - 即如果一个数字 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;博士

以 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 - 与原始值相比(对于许多应用程序而言)微不足道。

比较浮点数的最后几位本质上是危险的,因为任何读过著名的“What Every Computer Scientist Should Know About Floating-Point Arithmetic”(涵盖此答案的所有主要部分)的人都会知道。

大多数计算器使用额外的 guard digits 来解决这个问题,这就是 0.1 + 0.2 给出 0.3 的方式:最后几位被四舍五入。

答7:

一个优秀的自由职业者,应该有对需求敏感和精准需求捕获的能力,而huntsbot.com提供了这个机会

除了其他正确答案之外,您可能需要考虑缩放值以避免浮点运算问题。

例如:

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 在 JavaScript 中返回 false,但幸运的是浮点整数运算是精确的,因此可以通过缩放来避免十进制表示错误。

作为一个实际示例,为避免精度至关重要的浮点问题,建议1将货币处理为表示美分数量的整数:2550 美分而不是 25.50 美元。

1 道格拉斯·克罗克福德:JavaScript: The Good Parts: Appendix A - Awful Parts (page 105)。

问题是转换本身是不准确的。 16.08 * 100 = 1607.9999999999998。我们是否必须求助于拆分数字并单独转换(如 16 * 100 + 08 = 1608)?

此处的解决方案是以整数进行所有计算,然后除以您的比例(在本例中为 100)并仅在呈现数据时四舍五入。这将确保您的计算始终准确。

只是挑剔一点:整数运算仅在浮点数中精确到一个点(双关语)。如果该数字大于 0x1p53(使用 Java 7 的十六进制浮点表示法,= 9007199254740992),则此时 ulp 为 2,因此 0x1p53 + 1 向下舍入为 0x1p53(并且 0x1p53 + 3 向上舍入为 0x1p53 + 4、由于四舍五入)。 :-D 但是当然,如果你的数字小于 9 万亿,你应该没问题。 :-P

答8:

保持自己快人一步,享受全网独家提供的一站式外包任务、远程工作、创意产品订阅服务–huntsbot.com

存储在计算机中的浮点数由两部分组成,一个整数和一个指数,以指数为底并乘以整数部分。

如果计算机以 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⁻²,并将它们添加到 { 5} 或 0.75。确切地。

问题在于数字可以精确地以 10 为底,但不能以 2 为底。这些数字需要四舍五入到最接近的等值。假设非常常见的 IEEE 64 位浮点格式,最接近 0.1 的数字是 3602879701896397 x 2⁻⁵⁵,最接近 0.2 的数字是 7205759403792794 x 2⁻⁵⁵;将它们加在一起得到 10808639105689191 x 2⁻⁵⁵,或精确的十进制值 0.3000000000000000444089209850062616169452667236328125。浮点数通常四舍五入以便显示。

@Mark感谢您的清晰解释,但随后出现的问题是为什么 0.1+0.4 恰好等于 0.5 (atleast in Python 3) 。在 Python 3 中使用浮点数时检查相等性的最佳方法是什么?

@user2417881 IEEE 浮点运算对每个运算都有舍入规则,有时即使两个数字相差一点,舍入也能产生准确的答案。细节太长了,无法评论,而且我也不是他们的专家。正如您在此答案中看到的那样,0.5 是可以用二进制表示的少数小数之一,但这只是巧合。有关相等性测试,请参阅 stackoverflow.com/questions/5595425/…。

@user2417881 你的问题让我很感兴趣,所以我把它变成了一个完整的问答:stackoverflow.com/q/48374522/5987

答9:

huntsbot.com高效搞钱,一站式跟进超10+任务平台外包需求

浮点舍入误差。从 What Every Computer Scientist Should Know About Floating-Point Arithmetic:

将无限多个实数压缩为有限位数需要近似表示。尽管整数有无限多,但在大多数程序中,整数计算的结果可以存储在 32 位中。相反,给定任何固定位数,大多数实数计算将产生无法使用那么多位精确表示的量。因此,浮点计算的结果必须经常四舍五入以适应其有限表示。这种舍入误差是浮点计算的特征。

答10:

huntsbot.com提供全网独家一站式外包任务、远程工作、创意产品分享与订阅服务!

In short 这是因为:

浮点数不能用二进制精确表示所有小数

因此,就像 10/3 其中 does not exist 精确地以 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).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 点之后的那些不需要的零?现在是时候将其转换为 float 以使其如您所愿:

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)); } 函数 addUp(){ var number1 = +KaTeX parse error: Expected 'EOF', got '#' at position 3: ("#̲number1").val()…(“#number2”).val(); var unexpectedResult = number1 + number2; var expectedResult = floatify(number1 + number2); $(“#unexpectedResult”).text(unexpectedResult); $(“#expectedResult”).text(expectedResult); } 加起来();输入{宽度:50px; } #expectedResult{ 颜色:绿色; } #unexpectedResult{ 颜色:红色; } + = 预期结果: 意外结果:

你可以这样使用它:

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 根本不起作用,尽管它看起来一样!我更喜欢第一个解决方案,因为我可以将它用作将输入浮点数转换为准确输出浮点数的函数。

这让我很头疼。我将 12 个浮点数相加,然后显示这些数字的总和和平均值。使用 toFixed() 可能会修复 2 个数字的总和,但是当对多个数字求和时,飞跃很重要。

@Nuryagdy Mustapayev 我没有得到你的意图,因为我在你可以对 12 个浮点数求和之前进行了测试,然后对结果使用 floatify() 函数,然后做任何你想做的事情,我发现使用它没有问题。

我只是说在我的情况下,我有大约 20 个参数和 20 个公式,其中每个公式的结果取决于其他公式,这个解决方案没有帮助。

答11:

huntsbot.com高效搞钱,一站式跟进超10+任务平台外包需求

我的解决方法:

function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

精度是指在加法过程中要在小数点后保留的位数。

原文链接:https://www.huntsbot.com/qa/karg/is-floating-point-math-broken?lang=zh_CN&from=csdn

huntsbot.com提供全网独家一站式外包任务、远程工作、创意产品分享与订阅服务!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值