考虑以下代码:

0.1 + 0.2 == 0.3  ->  false
0.1 + 0.2         ->  0.30000000000000004

为什么会出现这些错误?


当前回答

正常的算术是以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) ...

其他回答

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

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

0.1000000000000000055511151231257827021181583404541015625(十进制),或0x1.999999999999ap-4,采用C99六进制浮点数表示法。

相比之下,有理数0.1(1/10)可以完全写成

0.1(十进制),或0x1.999999999999999…p-4,类似于C99十六进制浮点数,其中。。。表示9的无限序列。

程序中的常数0.2和0.3也将近似于其真实值。恰好最接近0.2的两倍大于有理数0.2,但最接近0.3的两倍小于有理数0.3。0.1和0.2的和最终大于有理数0.3,因此与代码中的常数不一致。

浮点运算问题的一个相当全面的处理是每个计算机科学家都应该知道的浮点运算。有关更容易理解的解释,请参阅floatingpoint-gui.de。

边注:所有位置(以N为基数)数字系统都有精度问题

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

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

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

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

边注:在编程中使用浮点

实际上,这种精度问题意味着在显示浮点数之前,需要使用舍入函数将浮点数舍入到您感兴趣的小数位数。

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

如果(x==y){…}则不执行

相反,如果(abs(x-y)<myToleranceValue){…},则执行此操作。

其中abs是绝对值。需要为您的特定应用程序选择myToleranceValue,这与您准备允许多少“摆动空间”以及您将要比较的最大值(由于精度损失问题)有很大关系。当心您选择的语言中的“epsilon”样式常量。这些值可以用作公差值,但它们的有效性取决于您使用的数字的大小,因为使用大数字的计算可能会超过epsilon阈值。

我的答案很长,所以我把它分成了三部分。因为这个问题是关于浮点数学的,所以我把重点放在了机器的实际功能上。我还将其指定为双精度(64位),但该参数同样适用于任何浮点运算。

序言

IEEE 754双精度二进制浮点格式(binary64)数字表示以下形式的数字

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

64位:

第一位是符号位:如果数字为负,则为1,否则为0。接下来的11位是指数,偏移1023。换句话说,在从双精度数字中读取指数位之后,必须减去1023以获得2的幂。剩下的52位是有效位(或尾数)。在尾数中,“隐含”1。由于任何二进制值的最高有效位为1,因此总是省略2。

1-IEEE 754允许有符号零的概念-+0和-0被不同地对待:1/(+0)是正无穷大;1/(-0)是负无穷大。对于零值,尾数和指数位均为零。注意:零值(+0和-0)未明确归为非标准2。

2-非正规数的情况并非如此,其偏移指数为零(以及隐含的0)。非正规双精度数的范围为dmin≤|x|≤dmax,其中dmin(最小的可表示非零数)为2-1023-51(≈4.94*10-324),dmax(最大的非正规数,其尾数完全由1组成)为2-1023+1-21-23-51(≈2.225*10-308)。


将双精度数字转换为二进制

存在许多在线转换器来将双精度浮点数转换为二进制(例如,在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版本,跳到底部)

卡托·约翰斯顿(提问者)问为什么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},我们将指数增加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仅在最后一位不同。。。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;博士

将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:最后几位是四舍五入的。

在硬件级别,浮点数表示为二进制数的分数(以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,因此舍入值降低。

如果您处于小数向下舍入的情况,那么应该使用十进制模块。顺便说一下,十进制模块还提供了一种方便的方式来“查看”为任何浮点存储的确切值。

>>> 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”的问题将在下文“表示错误”一节中详细解释。有关此类惊喜的更完整列表,请参阅浮点运算的危险。

确实没有简单的答案,但是不要对浮动虚拟数字过分怀疑!在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)

in :

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.100000000000000005551115123125。在Python 2.7和Python 3.1之前的版本中,Python舍入这些值​​到17位有效小数,显示“0.10000000000000001”。在当前版本的Python中,显示的值是分数尽可能短的值,当转换回二进制时,给出的表示形式完全相同,只需显示“0.1”。

硬件设计师的视角

我认为,既然我设计并构建了浮点硬件,我就应该添加一个硬件设计师的视角。了解错误的来源可能有助于了解软件中发生的情况,最终,我希望这有助于解释为什么浮点错误会发生并似乎会随着时间累积的原因。

1.概述

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

2.标准

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

在IEEE-754标准中,硬件设计者可以使用误差/ε的任何值,只要它在最后一个位置小于一个单位的一半,并且一次操作的结果只需要在最后一位小于一个单元的一半。这解释了为什么当重复操作时,错误会增加。对于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的最后一位使用舍入到最接近的偶数位的方法,因为随着时间的推移,误差更可能相互抵消,而区间算术结合了IEEE754舍入模式的变化来预测舍入误差,并对其进行校正。由于与其他舍入模式相比,其相对误差较低,因此舍入到最近的偶数位(最后一位)是IEEE-754的默认舍入模式。

请注意,默认舍入模式(舍入到最后一位最接近的偶数位)保证一次操作的误差小于最后一位单位的一半。仅使用截断、向上舍入和向下舍入可能会导致误差大于最后一位一个单位的一半,但小于最后一位的一个单位,因此不建议使用这些模式,除非它们用于区间算术。

6.总结

简而言之,浮点运算中出现错误的根本原因是硬件中的截断和除法中倒数的截断。由于IEEE-754标准只要求单个操作的误差小于最后一位一个单位的一半,因此重复操作中的浮点误差将相加,除非得到纠正。

其实很简单。当你有一个基数为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/