考虑以下代码:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
为什么会出现这些不准确的情况?
我相信我应该添加硬件设计师的观点,因为我设计和构建浮点硬件。了解错误的根源可能有助于理解软件中发生的情况,最终,我希望这有助于解释浮点错误发生并似乎随着时间的推移而累积的原因。
从工程角度来看,大多数浮点运算都会存在一些误差,因为进行浮点计算的硬件只需要在最后一位的误差小于一个单位的二分之一。因此,许多硬件将停止在一个精度,该精度仅需要在单个操作的最后一位产生小于一个单位的误差,这在浮点除法中尤其成问题。单个操作的构成取决于该单元需要多少个操作数。对于大多数来说,它是两个,但有些单元需要 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 位(加上一些可选位)。
Z=X/Y
Z = X * (1/Y)
k>=2
2+2= 4
3.1 除法舍入误差:倒数的近似
商选择表中的倒数取决于除法:慢速除法如SRT除法,或快速除法如Goldschmidt除法;每个条目都会根据除法算法进行修改,以尝试产生尽可能低的错误。但无论如何,所有倒数都是实际倒数的近似值,并且会引入一些误差元素。慢速除法和快速除法都迭代计算商,即每一步计算商的一些位数,然后从被除数中减去结果,除法器重复这些步骤,直到误差小于二分之一单位排在最后一位。慢速除法方法在每个步骤中计算固定位数的商,并且通常构建成本较低,而快速除法方法在每个步骤中计算可变位数,并且通常构建成本更高。除法最重要的部分是,它们大多数依赖于倒数的近似值的重复乘法,因此很容易出错。
所有操作中出现舍入错误的另一个原因是 IEEE-754 允许的最终答案的不同截断模式。有截断、向零舍入、舍入到最近(默认)、 舍入-向下,向上舍入。对于单个操作,所有方法都会在最后引入小于一个单位的误差元素。随着时间的推移和重复的操作,截断也会累积地增加最终的误差。这种截断错误在求幂时尤其成问题,因为求幂涉及某种形式的重复乘法。
由于进行浮点计算的硬件在一次运算中只需要在最后一位产生误差小于二分之一的结果,因此如果不注意,误差将随着重复运算而增大。这就是在需要有界误差的计算中,数学家使用诸如使用舍入到最近的方法区间算术与 IEEE 的变体相结合754 舍入模式来预测舍入误差并纠正它们。由于与其他舍入模式相比相对误差较低,因此舍入到最接近的偶数位(最后一位)是 IEEE-754 的默认舍入模式。
请注意,默认舍入模式,舍入到最近的最后一位的偶数 a>,保证一次运算最后一位的误差小于二分之一。单独使用截断、向上取整、向下取整可能会导致误差大于最后一位的二分之一,但小于最后一位的一个单位,所以不推荐使用这些模式,除非是用于区间算术。
简而言之,浮点运算出错的根本原因是硬件截断和除法时倒数截断的结合。由于IEEE-754标准只要求单次运算的最后一位的误差小于二分之一,因此重复运算的浮点误差将会累加,除非进行纠正。
二进制浮点数学就像这样。在大多数编程语言中,它基于 IEEE 754 标准。问题的关键在于,数字以这种格式表示为整数乘以 2 的幂;分母不是2的幂的有理数(例如0.1,即1/10)无法精确表示。
0.1
1/10
对于标准 binary64 格式的 0.1,其表示形式可以完全写为
binary64
0.10000000000000000055511151231257827021181583404541015625
0x1.999999999999ap-4
相比之下,有理数0.1,即1/10,可以完全写成
0x1.99999999999999...p-4
...
程序中的常量0.2和0.3也将是其真实值的近似值。碰巧,最接近 0.2 的 double 大于有理数 0.2,但最接近 double code>0.3 小于有理数 0.3。 0.1 和 0.2 的总和最终大于有理数 0.3,因此与代码中的常量不一致。
0.2
0.3
double
浮点算术问题的相当全面的处理是每个计算机科学家都应该了解浮点运算。有关更容易理解的解释,请参阅 floating-point-gui.de。
普通的旧十进制(以 10 为基数)数字也有同样的问题,这就是为什么像 1/3 这样的数字最终会变成 0.333333333...
您刚刚偶然发现了一个数字 (3/10),它很容易用十进制表示,但不适合二进制系统。它也是双向的(在某种程度上):1/16 在十进制中是一个丑陋的数字(0.0625),但在二进制中它看起来像十进制的第 10,000 个(0.0001)** - 如果我们在由于我们在日常生活中使用以 2 为基数的数字系统的习惯,您甚至会看到这个数字并本能地理解您可以通过将某个东西减半、再减半、一次又一次地达到这个数字。
当然,这并不完全是浮点数在内存中的存储方式(它们使用科学计数法的形式)。然而,它确实说明了二进制浮点精度误差往往会出现,因为我们通常感兴趣的“现实世界”数字通常是十的幂 - 但这只是因为我们使用十进制数字系统日 -今天。这也是为什么我们会说 71% 而不是“每 7 中就有 5”(71% 是一个近似值,因为 5/7 无法用任何十进制数字精确表示)。
所以不:二进制浮点数并没有被破坏,它们只是碰巧和其他所有基于 N 的数字系统一样不完美:)
实际上,这种精度问题意味着您需要使用舍入函数将浮点数四舍五入到您感兴趣的小数位数,然后再显示它们。
您还需要用允许一定程度容差的比较来替换相等测试,这意味着:
不要不做if (x == y) { ... }
if (x == y) { ... }
而是执行 if (abs(x - y) 。
if (abs(x - y) 。
其中 abs 是绝对值。 myToleranceValue 需要根据您的特定应用程序进行选择 - 这与您准备允许的“回旋空间”有很大关系,以及您要比较的最大数字可能是多少是(由于精度损失问题)。请注意您选择的语言中的“epsilon”样式常量。这些可以用作容差值,但它们的有效性取决于您正在使用的数字的大小(大小),因为大数字的计算可能会超出 epsilon 阈值。
abs
myToleranceValue
硬件设计师的视角
我相信我应该添加硬件设计师的观点,因为我设计和构建浮点硬件。了解错误的根源可能有助于理解软件中发生的情况,最终,我希望这有助于解释浮点错误发生并似乎随着时间的推移而累积的原因。
1。概述
从工程角度来看,大多数浮点运算都会存在一些误差,因为进行浮点计算的硬件只需要在最后一位的误差小于一个单位的二分之一。因此,许多硬件将停止在一个精度,该精度仅需要在单个操作的最后一位产生小于一个单位的误差,这在浮点除法中尤其成问题。单个操作的构成取决于该单元需要多少个操作数。对于大多数来说,它是两个,但有些单元需要 3 个或更多操作数。因此,不能保证重复操作会导致所需的错误,因为错误会随着时间的推移而累积。
2。标准
大多数处理器遵循 IEEE-754 标准,但有些处理器使用非规范化或不同的标准 。例如,IEEE-754 中有一种非规范化模式,它允许以牺牲精度为代价来表示非常小的浮点数。不过,下面将介绍 IEEE-754 的标准化模式,这是典型的操作模式。
在 IEEE-754 标准中,硬件设计者允许使用任何 error/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 的默认舍入模式。
请注意,默认舍入模式,舍入到最近的最后一位的偶数 a>,保证一次运算最后一位的误差小于二分之一。单独使用截断、向上取整、向下取整可能会导致误差大于最后一位的二分之一,但小于最后一位的一个单位,所以不推荐使用这些模式,除非是用于区间算术。
6。摘要
简而言之,浮点运算出错的根本原因是硬件截断和除法时倒数截断的结合。由于IEEE-754标准只要求单次运算的最后一位的误差小于二分之一,因此重复运算的浮点误差将会累加,除非进行纠正。
二进制浮点数学就像这样。在大多数编程语言中,它基于 IEEE 754 标准。问题的关键在于,数字以这种格式表示为整数乘以 2 的幂;分母不是2的幂的有理数(例如
0.1
,即1/10
)无法精确表示。对于标准
binary64
格式的0.1
,其表示形式可以完全写为0.10000000000000000055511151231257827021181583404541015625
(十进制),或0x1.999999999999ap-4
C99 十六进制浮点表示法.相比之下,有理数
0.1
,即1/10
,可以完全写成0.1
(十进制),或0x1.99999999999999...p-4
类似于 C99 十六进制浮点表示法,其中...
表示无休止的 9 序列。程序中的常量
0.2
和0.3
也将是其真实值的近似值。碰巧,最接近0.2
的double
大于有理数0.2
,但最接近double
code>0.3 小于有理数0.3
。0.1
和0.2
的总和最终大于有理数0.3
,因此与代码中的常量不一致。浮点算术问题的相当全面的处理是每个计算机科学家都应该了解浮点运算。有关更容易理解的解释,请参阅 floating-point-gui.de。
普通的旧十进制(以 10 为基数)数字也有同样的问题,这就是为什么像 1/3 这样的数字最终会变成 0.333333333...
您刚刚偶然发现了一个数字 (3/10),它很容易用十进制表示,但不适合二进制系统。它也是双向的(在某种程度上):1/16 在十进制中是一个丑陋的数字(0.0625),但在二进制中它看起来像十进制的第 10,000 个(0.0001)** - 如果我们在由于我们在日常生活中使用以 2 为基数的数字系统的习惯,您甚至会看到这个数字并本能地理解您可以通过将某个东西减半、再减半、一次又一次地达到这个数字。
当然,这并不完全是浮点数在内存中的存储方式(它们使用科学计数法的形式)。然而,它确实说明了二进制浮点精度误差往往会出现,因为我们通常感兴趣的“现实世界”数字通常是十的幂 - 但这只是因为我们使用十进制数字系统日 -今天。这也是为什么我们会说 71% 而不是“每 7 中就有 5”(71% 是一个近似值,因为 5/7 无法用任何十进制数字精确表示)。
所以不:二进制浮点数并没有被破坏,它们只是碰巧和其他所有基于 N 的数字系统一样不完美:)
实际上,这种精度问题意味着您需要使用舍入函数将浮点数四舍五入到您感兴趣的小数位数,然后再显示它们。
您还需要用允许一定程度容差的比较来替换相等测试,这意味着:
不要不做
if (x == y) { ... }
而是执行
if (abs(x - y) 。
其中
abs
是绝对值。myToleranceValue
需要根据您的特定应用程序进行选择 - 这与您准备允许的“回旋空间”有很大关系,以及您要比较的最大数字可能是多少是(由于精度损失问题)。请注意您选择的语言中的“epsilon”样式常量。这些可以用作容差值,但它们的有效性取决于您正在使用的数字的大小(大小),因为大数字的计算可能会超出 epsilon 阈值。