考虑以下代码:

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

为什么会出现这些错误?


当前回答

我可以补充一下吗;人们总是认为这是一个计算机问题,但如果你用手(以10为基数)计算,你就不能得到(1/3+1/3=2/3)=真,除非你有无穷大可以将0.333…加到0.333……就像(1/10+2/10)一样==基数2的3/10问题,您将其截断为0.333+0.333=0.666,并可能将其舍入为0.667,这在技术上也是不准确的。

用三进制数,三分之三不是问题——也许有人会问为什么你的十进制数学被打破了。。。

其他回答

你试过胶带解决方案了吗?

尝试确定错误发生的时间,并用简短的if语句修复它们,这并不漂亮,但对于某些问题,这是唯一的解决方案,这就是其中之一。

 if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
                    else { return n * 0.1 + 0.000000000000001 ;}    

我在c#的一个科学模拟项目中也遇到过同样的问题,我可以告诉你,如果你忽视蝴蝶效应,它会变成一条大胖龙,咬你一口**

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

例如:

var result = 1.0 + 2.0;     // result === 3.0 returns true

…而不是:

var result = 0.1 + 0.2;     // result === 0.3 returns false

在JavaScript中,表达式0.1+0.2===0.3返回false,但幸运的是,浮点中的整数运算是精确的,因此可以通过缩放来避免十进制表示错误。

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


1 Douglas Crockford:JavaScript:好的部分:附录A——糟糕的部分(第105页)。

我的答案很长,所以我把它分成了三部分。因为这个问题是关于浮点数学的,所以我把重点放在了机器的实际功能上。我还将其指定为双精度(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:最后几位是四舍五入的。

硬件设计师的视角

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

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标准只要求单个操作的误差小于最后一位一个单位的一半,因此重复操作中的浮点误差将相加,除非得到纠正。

十进制数(如0.1、0.2和0.3)在二进制编码浮点类型中没有精确表示。0.1和0.2的近似值之和与0.3的近似值不同,因此,0.1+0.2==0.3的错误在这里可以更清楚地看到:

#include <stdio.h>

int main() {
    printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
    printf("0.1 is %.23f\n", 0.1);
    printf("0.2 is %.23f\n", 0.2);
    printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
    printf("0.3 is %.23f\n", 0.3);
    printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
    return 0;
}

输出:

0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17

为了更可靠地计算这些计算,您需要对浮点值使用基于十进制的表示。C标准没有默认指定此类类型,而是作为技术报告中描述的扩展。

_Decimal32、_Decimal64和_Decimal128类型可能在您的系统上可用(例如,GCC在选定的目标上支持它们,但Clang在OS X上不支持它们)。