考虑以下代码:

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

为什么会出现这些错误?


当前回答

二进制浮点数学是这样的。在大多数编程语言中,它基于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:最后几位是四舍五入的。

它被打破的方式与你在小学学习并每天使用的十进制(以10为基础)表示法完全相同,只是以2为基础。

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

以同样的方式,1/10(十进制0.1)不能以2为基数(二进制)精确地表示为“十进制”值;小数点后的重复模式将永远持续下去。该值不精确,因此无法使用常规浮点方法对其进行精确计算。与基数10一样,还有其他值也显示了这个问题。

这个问题的许多重复问题都是关于浮点舍入对特定数字的影响。在实践中,通过查看感兴趣的计算的确切结果而不是仅仅阅读它,更容易了解它的工作原理。一些语言提供了实现这一点的方法,例如在Java中将浮点或双精度转换为BigDecimal。

由于这是一个语言不可知的问题,因此需要语言不可知工具,例如十进制到浮点转换器。

将其应用于问题中的数字,视为双精度:

0.1转换为0.1000000000000000055511151231257827021181583404541015625,

0.2转换为0.200000000000000011102230246251565404236316680908203125,

0.3转换为0.299999999999999988897769753748434595763683319091796875,以及

0.300000000000000004转换为0.30000000000000000444089209850062616169452667236328125。

手动或在十进制计算器(如Full Precision calculator)中添加前两个数字,显示实际输入的精确和为0.30000000000000000166533453693773481063544750213623046875。

如果四舍五入到等于0.3,则舍入误差将为0.000000000000000027755575615628913510591702705078125。四舍五入等于0.300000000000000004也会产生舍入误差0.000000000000000027755575615628913510591702705078125。打成平手的规则适用。

返回浮点转换器,0.300000000000000004的原始十六进制是3fd333333333334,以偶数结尾,因此是正确的结果。

浮点舍入错误。从每个计算机科学家应该知道的浮点运算:

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

这些奇怪的数字之所以出现,是因为计算机使用二进制(以2为基数)数字系统进行计算,而我们使用十进制(以10为基数)。

大多数分数不能用二进制或十进制或两者精确表示。结果-四舍五入(但精确)的数字结果。