赞
踩
考虑以下代码:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
为什么会出现这些错误?
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 人丧生。
huntsbot.com – 程序员副业首选,一站式外包任务、远程工作、创意产品分享订阅平台。
硬件设计师的观点
我相信我应该为此添加硬件设计师的观点,因为我设计和构建浮点硬件。了解错误的来源可能有助于理解软件中发生的事情,最终,我希望这有助于解释浮点错误发生的原因,并且似乎随着时间的推移而累积。
一、概述
从工程的角度来看,大多数浮点运算都会有一些错误,因为进行浮点计算的硬件只需要在最后一个单位的误差小于一半。因此,许多硬件将停止在一个精度上,该精度只需要在单个操作的最后一个位置产生小于一半的误差,这在浮点除法中尤其成问题。什么构成单个操作取决于该单元需要多少个操作数。大多数情况下,它是两个,但有些单元需要 3 个或更多操作数。因此,不能保证重复的操作会导致理想的错误,因为错误会随着时间的推移而累积。
大多数处理器遵循 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 除法;每个条目都根据除法算法进行修改,以尝试产生尽可能低的错误。但是,无论如何,所有倒数都是实际倒数的近似值,并引入了一些误差因素。慢除法和快除法都迭代计算商,即每一步计算商的一些位数,然后从被除数中减去结果,除法器重复这些步骤,直到误差小于一的二分之一单位排在最后。慢除法方法在每个步骤中计算商的固定位数并且通常构建成本较低,而快速除法方法计算每个步骤的可变位数并且通常构建成本更高。除法方法最重要的部分是它们中的大多数依赖于重复乘以一个倒数的近似,因此它们容易出错。
所有操作中舍入误差的另一个原因是 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。但是,这会重复移动并从被除数中减去除数,并且需要许多时钟周期,因为它只计算每个时钟周期的商的一位。我们使用倒数表,以便我们可以计算每个周期的商的更多位,并进行有效的性能/速度权衡。
与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 浮点表示。这样做只会实现一件事:更快地计算错误的结果。
huntsbot.com全球7大洲远程工作机会,探索不一样的工作方式
这里的大多数答案都用非常枯燥的技术术语来解决这个问题。我想用普通人可以理解的方式来解决这个问题。
想象一下,您正在尝试切比萨饼。你有一个机器人披萨切割机,可以将披萨片精确地切成两半。它可以将整个披萨减半,也可以将现有切片减半,但无论如何,减半总是准确的。
那个比萨刀的动作非常精细,如果你从一整块比萨开始,然后把它减半,然后每次将最小的切片继续减半,你可以减半 53 次,直到切片太小,甚至无法达到高精度的能力.那时,您不能再将那个非常薄的切片减半,而必须按原样包含或排除它。
现在,您将如何将所有切片以这样一种方式拼凑起来,加起来相当于披萨的十分之一 (0.1) 或五分之一 (0.2)?认真想想,然后努力解决。如果您手头有一个神话般的精密比萨刀,您甚至可以尝试使用真正的比萨饼。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。