浮点数学坏了吗?

考虑以下代码:

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

为什么会出现这些不准确的情况?

511329 次浏览

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

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

  • 0.1000000000000000055511151231257827021181583404541015625小数,或
  • 0x1.999999999999ap-4C99六进制记数法

相反,有理数0.1,也就是1/10,可以写成

  • 0.1小数,或
  • 0x1.99999999999999...p-4在C99六进制表示法的模拟中,其中...表示9的无休止序列。

程序中的常数0.20.3也将是它们真实值的近似值。碰巧最接近double0.2的数字大于有理数0.2,但最接近double0.3的数字小于有理数0.30.10.2的总和最终大于有理数0.3,因此与代码中的常数不一致。

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

附注:所有位置(基数N)数系统都有精度问题

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

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

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

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

旁注:在编程中使用浮点数

在实践中,这个精度问题意味着您需要使用舍入函数将浮点数舍入到您感兴趣的小数位数,然后再显示它们。

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

没有if (x == y) { ... }

相反,做if (abs(x - y) < myToleranceValue) { ... }

其中abs是绝对值。myToleranceValue需要为您的特定应用程序选择-它与您准备允许多少“摆动空间”以及您将比较的最大数字可能是什么(由于精度问题的损失)有很大关系。小心您选择的语言中的“epsilon”样式常量。这些是用作容差值的没有

浮点舍入误差。由于缺少5的素数因子,0.1在2进制中不能像在10进制中那样准确地表示。就像1/3需要无限多的数字来表示小数,但在3进制中是“0.1”,0.1在2进制中需要无限多的数字,而在10进制中没有。计算机没有无限量的内存。

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

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

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

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

同样,1/10(十进制0.1)不能精确地以2进制(二进制)表示为“十进制”值;小数点后的重复模式永远持续下去。该值不精确,因此您无法使用普通浮点方法对其进行精确的数学运算。就像基数10一样,还有其他值也会出现这个问题。

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

例如:

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

…而不是:

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

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

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


1道格拉斯·克罗克福德:JavaScript:好的部分:附录A-可怕的部分(第105页)

我的解决方法:

function add(a, b, precision) {var x = Math.pow(10, precision || 2);return (Math.round(a * x) + Math.round(b * x)) / x;}

精度是指在加法过程中要保留在小数点后的位数。

你试过胶带解决方案吗?

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

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

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

硬件设计师的视角

我认为我应该添加一个硬件设计师的观点,因为我设计和构建浮点硬件。知道错误的起源可能有助于理解软件中发生的事情,最终,我希望这有助于解释为什么浮点错误发生并似乎随着时间的推移而积累的原因。

1.概述

从工程的角度来看,大多数浮点运算都会有一些误差因素,因为进行浮点计算的硬件只需要在最后一个单元的误差小于一半。因此,许多硬件将停止在一个精度上,这对于单一操作来说只有在最后一个单元的误差小于一半时才是必要的,这在浮点除法中尤其有问题。构成单个操作的内容取决于该单元需要多少操作数。对于大多数,它是两个,但有些单元需要3个或更多操作数。因此,无法保证重复操作会导致理想的错误,因为错误会随着时间的推移而累积。

2.标准

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

在IEEE-754标准中,允许硬件设计人员使用任何错误/epsilon值,只要它小于最后一个单元的一半,并且一次操作的结果只需小于最后一个单元的一半。这解释了为什么当有重复操作时,错误会累加。对于IEEE-754双精度,这是第54位,因为53位用于表示浮点数的数字部分(标准化),也称为尾数(例如5.3e5中的5.3)。接下来的部分将更详细地介绍各种浮点操作中硬件错误的原因。

3.除法舍入误差的原因

浮点除法误差的主要原因是用于计算商的除法算法。大多数计算机系统使用乘以逆来计算除法,主要是在Z=X/YZ = X * (1/Y)中。除法是迭代计算的,即每个周期计算商的一些位,直到达到所需的精度,对于IEEE-754来说,这是误差小于最后一个单位的任何东西。Y(1/Y)的倒数表在慢除法中称为商选择表(QST),商选择表的大小(以位为单位)通常是基数的宽度,或在每次迭代中计算的商的位数,加上一些保护位。对于IEEE-754标准,双精度(64-bit),它将是除法器的基数大小,加上一些保护位k,其中k>=2。因此,例如,一次计算商的2位(基数4)的除法器的典型商选择表将是2+2= 4位(加上一些可选位)。

3.1除法舍入误差:倒数的近似

商选择表中的倒数取决于除法:慢除法,如SRT除法,或快速除法,如Goldschmidt除法;根据除法算法修改每个条目,试图产生尽可能低的误差。然而,在任何情况下,所有倒数都是实际倒数的近似,并引入一些误差元素。慢除法和快除法都迭代计算商,即每一步计算商的一些位数,然后从红利中减去结果,除法器重复这些步骤,直到误差小于最后一个单位的一半。慢除法在每一步计算商的固定位数,通常构建成本较低,而快除法在每一步计算可变位数,通常构建成本较高。除法最重要的部分是,它们中的大多数依赖于重复乘以倒数的近似,因此它们容易出错。

4.其他操作中的舍入错误:截断

所有操作中舍入误差的另一个原因是IEEE-754允许的最终答案的不同截断模式。有截断、向零舍入、舍入到最近(默认),向下舍入和向上舍入。所有方法都在单个操作的最后一位引入了小于一个单位的误差元素。随着时间的推移和重复的操作,截断还会累积增加所得到的误差。这种截断误差在指数运算中尤其成问题,指数运算涉及某种形式的重复乘法。

5.重复操作

由于执行浮点计算的硬件只需要在一次操作中产生误差小于最后一个单位的一半的结果,如果不观察,误差会随着重复操作而增加。这就是为什么在需要有界误差的计算中,数学家使用IEEE-754的舍入到最近最后一位的偶数等方法,因为随着时间的推移,错误更有可能相互抵消,区间算术结合IEEE 754舍入模式的变化来预测舍入误差并纠正它们。由于与其他舍入模式相比其相对误差较低,舍入到最接近的偶数位(最后一位)是IEEE-754的默认舍入模式。

请注意,默认舍入模式,舍入到最近的最后一位的偶数,保证一次操作的误差小于最后一个单位的一半。单独使用截断、舍入和舍入可能会导致误差大于最后一个单位的一半,但小于最后一个单位,因此不推荐使用这些模式,除非它们在Interval Arithmetic中使用。

6.摘要

简而言之,浮点运算误差的根本原因是硬件中的截断和除法情况下的倒数截断的组合。由于IEEE-754标准只要求单个操作的误差小于最后一个单位的一半,除非纠正,否则重复操作的浮点误差将加起来。

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

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

已经发布了很多好的答案,但我想再添加一个。

不是所有的数字都可以用漂浮/双精度来表示例如,数字“0.2”将在IEEE754浮点标准中单精度表示为“0.200000003”。

在引擎盖下存储实数的模型将浮点数表示为

在此处输入图片描述

即使您可以轻松键入0.2,但对于使用“IEEE二进制浮点运算标准(ISO/IEEE Std 754-1985)”的带有FPU的计算机,FLT_RADIXDBL_RADIX是2;而不是10。

因此,准确表示这些数字有点困难。即使您在没有任何中间计算的情况下显式指定此变量。

这里的大多数答案都用非常枯燥的技术术语来解决这个问题。我想用普通人可以理解的术语来解决这个问题。

想象一下,你正在尝试切披萨。你有一个机器人披萨切割机,可以将披萨切成两半。它可以将整个披萨减半,也可以将现有切片减半,但无论如何,减半总是精确的。

比萨饼切割机有非常精细的运动,如果你从一个完整的比萨饼开始,然后将其减半,并继续每次将最小的切片减半,你可以在切片太小甚至无法实现其高精度能力之前进行减半53次。在这一点上,你不能再将非常薄的切片减半,但必须按原样包含或排除它。

现在,你如何将所有的切片以这样的方式加起来等于披萨的十分之一(0.1)或五分之一(0.2)?真的想一想,并尝试解决它。如果你手头有一个神话般的精密披萨切割机,你甚至可以尝试使用真正的披萨。:-)


当然,大多数有经验的程序员都知道真正的答案,那就是没有办法用这些切片拼凑出披萨的十分之一或五分之一,无论你把它们切成多细。你可以做一个很好的近似值,如果你把0.1的近似值和0.2的近似值相加,你会得到一个很好的近似值0.3,但它仍然只是一个近似值。

对于双精度数字(允许您将披萨减半53次的精度),立即小于和大于0.1的数字是0.09999999999999999167332731531132594682276248931884765625和0.1000000000000000055511151231257827021181583404541015625。后者比前者更接近0.1,因此给定0.1的输入,数字解析器将倾向于后者。

(这两个数字之间的差异是“最小切片”,我们必须决定要么包含,这会引入向上偏差,要么排除,这会引入向下偏差。该最小切片的技术术语是ulp。)

在0.2的情况下,数字都是一样的,只是放大了2倍。同样,我们倾向于略高于0.2的值。

请注意,在这两种情况下,0.1和0.2的近似值都有轻微的向上偏差。如果我们添加足够多的这些偏差,它们会使数字离我们想要的越来越远,事实上,在0.1+0.2的情况下,偏差足够高,以至于结果数字不再是最接近0.3的数字。

特别是,0.1+0.2实际上是0.1000000000000000055511151231257827021181583404541015625+0.200000000000000011102230246251565404236316680908203125=0.3000000000000000444089209850062616169452667236328125,而最接近0.3的数字实际上是0.299999999999999988897769753748434595763683319091796875。


P. S.一些编程语言还提供了可以切成精确的十分之一的披萨切割器。虽然这种披萨切割器并不常见,但如果您确实可以使用它,那么当能够获得十分之一或五分之一切片时,您应该使用它。

(最初发布在Quora上)

一些统计数据与这个著名的双精度问题有关。

当使用0.1的步骤(从0.1到100)添加所有值(a+b)时,我们有~15%的精度误差。请注意,错误可能会导致略大或更小的值。下面是一些例子:

0.1 + 0.2 = 0.30000000000000004 (BIGGER)0.1 + 0.7 = 0.7999999999999999 (SMALLER)...1.7 + 1.9 = 3.5999999999999996 (SMALLER)1.7 + 2.2 = 3.9000000000000004 (BIGGER)...3.2 + 3.6 = 6.800000000000001 (BIGGER)3.2 + 4.4 = 7.6000000000000005 (BIGGER)

当使用0.1(从100到0.1)的步骤减去所有值(a-b其中a>b)时,我们有~34%的精度误差。下面是一些例子:

0.6 - 0.2 = 0.39999999999999997 (SMALLER)0.5 - 0.4 = 0.09999999999999998 (SMALLER)...2.1 - 0.2 = 1.9000000000000001 (BIGGER)2.0 - 1.9 = 0.10000000000000009 (BIGGER)...100 - 99.9 = 0.09999999999999432 (SMALLER)100 - 99.8 = 0.20000000000000284 (BIGGER)

*15%和34%确实很大,所以当精度非常重要时,请始终使用BigDecimal。使用2位十进制数字(步骤0.01),情况会更糟一些(18%和36%)。

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

序言

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

value=(-1)^s*(1.m51m50… m2m1m2*2e-1023

在64位:

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

1-IEEE 754允许符号零的概念-+0-0被区别对待:1 / (+0)是正无穷大;1 / (-0)是负无穷大。对于零值,尾数和指数位都是零。注意:零值(+0和-0)显式不归类为正态2

2-正态数不是这种情况,它的偏移指数为零(隐含的0.)。正态双精度数的范围是d分钟≤|x|≤d最大值,其中d分钟(最小的可表示非零数)是2-1023-51(约4.94*10-324),d最大值(最大的正态数,尾数完全由1组成)是210-2-1023-51(约2.225*1012)。


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

许多在线转换器将双精度浮点数转换为二进制数(例如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);}

进入主题:最初的问题

(跳到底部太长别读)

卡托约翰斯顿(提问者)问为什么0.1+0.2!=0.3。

以二进制形式编写(冒号分隔三个部分),IEEE 754表示的值是:

0.1 => 0:01111111011:10011001100110011001100110011001100110011001100110100.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].10011001100110011001100110011001100110011001100110100.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010or0.1 => 2^-56 * 7205759403792794 = 0.10000000000000000555111512312578270211815834045410156250.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125

要添加两个数字,指数需要相同,即:

0.1 => 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)0.2 => 2^-3 *  1.1001100110011001100110011001100110011001100110011010sum =  2^-3 * 10.0110011001100110011001100110011001100110011001100111or0.1 => 2^-55 * 3602879701896397  = 0.10000000000000000555111512312578270211815834045410156250.2 => 2^-55 * 7205759403792794  = 0.200000000000000011102230246251565404236316680908203125sum =  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落在两个值一个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

请注意,一个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允许的数字的最准确表示。由于默认舍入模式,添加这些表示会导致仅在最低有效位上不同的值。

太长别读

在IEEE 754二进制表示中编写0.1 + 0.2(冒号分隔三个部分)并将其与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的方式:最后几个比特被舍入。

鉴于没有人提到这一点…

一些高级语言,如Python和Java提供了克服二进制浮点限制的工具。例如:

这些解决方案都不是完美的(特别是如果我们看性能,或者如果我们需要非常高的精度),但它们仍然解决了二进制浮点运算的大量问题。

可以在数字计算机中实现的那种浮点数学必须使用实数的近似值和对它们的操作。(标准版本运行超过50页的留档,并有一个委员会来处理其勘误和进一步细化。)

这种近似是不同类型近似的混合,由于其偏离精确度的特定方式,每种近似都可以被忽略或仔细解释。它还涉及硬件和软件层面的许多明显的例外情况,大多数人假装没有注意到这些情况。

如果您需要无限精度(例如,使用数字π而不是其许多较短的替身之一),您应该编写或使用符号数学程序。

但是,如果您可以接受这样的想法,即有时浮点数学在值和逻辑上是模糊的,错误会迅速积累,并且您可以编写需求和测试来允许这一点,那么您的代码可以经常使用FPU中的内容。

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

由于这是一个与语言无关的问题,它需要与语言无关的工具,例如十进制到浮点转换器

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

0.1转换为0.1000000000000000055511151231257827021181583404541015625,

0.2转换为0.200000000000000011102230246251565404236316680908203125,

0.3转换为0.299999999999999988897769753748434595763683319091796875

0.30000000000000004转换为0.3000000000000000444089209850062616169452667236328125。

手动或在十进制计算器(如全精度计算器)中添加前两个数字,显示实际输入的确切总和为0.3000000000000000166533453693773481063544750213623046875。

如果向下舍入到0.3的等效值,则舍入误差为0.0000000000000000277555756156289135105907917022705078125。向上舍入到0.30000000000000004的等效值,则舍入误差为0.0000000000000000277555756156289135105907917022705078125。

回到浮点转换器,0.30000000000000004的原始十六进制是3fd33333333334,它以偶数结尾,因此是正确的结果。

不,不能断,但大多数小数必须近似

总结

浮点算术精确,不幸的是,它与我们通常的以10为基数的数字表示不匹配,所以事实证明我们经常给它输入与我们写的略有不同的输入。

即使是像0.01,0.02,0.03,0.04…0.24这样的简单数字也不能完全用二进制分数来表示。如果你把0.01,.02,.03………加起来,直到你达到0.25,你才能得到第一个可以在基数2中表示的分数。如果你用FP尝试这样做,你的0.01会稍微偏离一点,所以把25个数字加起来到一个精确的0.25的唯一方法需要一长串因果关系,包括保护位和舍入。很难预测,所以我们举起手说"FP不精确",,但事实并非如此。

我们不断地给FP硬件一些在基数10中看起来很简单但在基数2中是重复分数的东西。

这是怎么发生的?

当我们写小数时,每个分数(具体来说,每个终止小数)都是形式的有理数

a/(2n x 5m

在二进制中,我们只得到2n项,即:

a/2n

所以在十进制中,我们不能表示1/3。因为以10为基数包含2作为质因数,所以我们可以写成二进制分数的每个数字都可以写成以10为基数的分数。然而,我们写成的以10为基数的分数几乎没有任何东西可以用二进制表示。在0.01、0.02、0.03…0.99的范围内,只有的数字可以用我们的FP格式表示:0.25、0.50和0.75,因为它们是1/4、1/2和3/4,所有具有质因数的数字只使用2n项。

在基数10中,我们不能表示1/3。但是在二进制中,我们不能做1/101/3

因此,虽然每个二进制小数都可以用十进制写成,但反之亦然。事实上,大多数十进制小数都以二进制形式重复。

处理它

开发人员通常被指示进行比较,更好的建议可能是四舍五入到整数值(在C库中:round()和roundf(),即保持FP格式),然后进行比较。四舍五入到特定的十进制分数长度可以解决大多数输出问题。

此外,在实数运算问题(FP是在早期非常昂贵的计算机上发明的问题)中,宇宙的物理常数和所有其他测量值只有相对较少的有效数字知道,所以无论如何,整个问题空间都是“不精确的”。FP“精度”在这种应用中不是问题。

当人们尝试使用FP进行bean计数时,整个问题就出现了。它确实有效,但前提是你坚持使用整数值,这有点违背了使用它的意义。这就是为什么我们有所有这些十进制分数软件库。

我喜欢披萨的答案克里斯,因为它描述了实际问题,而不仅仅是通常对“不准确”的摆手。如果FP简单地“不准确”,我们可以在几十年前就这样做了。我们没有这样做的原因是FP格式紧凑快速,是处理大量数字的最佳方式。此外,这是太空时代和军备竞赛的遗产,也是早期尝试使用小型内存系统解决速度非常慢的计算机的大问题的遗产。(有时,单个磁芯用于1位存储,但这是另一个故事。

结论

如果你只是在银行数豆子,首先使用十进制字符串表示的软件解决方案可以很好地工作。但你不能用这种方式做量子色动力学或空气动力学。

存储在计算机中的浮点数由两部分组成,一个整数和一个指数,基数被取到并乘以整数部分。

如果计算机以10为基数工作,0.1将是1 x 10⁻¹0.2将是2 x 10⁻¹0.3将是3 x 10⁻¹。整数数学简单而精确,因此添加0.1 + 0.2显然会导致0.3

计算机通常不在10进制下工作,它们在2进制下工作。您仍然可以获得某些值的确切结果,例如0.51 x 2⁻¹0.251 x 2⁻²,将它们相加会导致3 x 2⁻²0.75。完全正确。

问题在于可以精确地以基数10表示的数字,但不能以基数2表示。这些数字需要四舍五入到最接近的等价物。假设非常常见的IEEE 64位浮点格式,最接近0.1的数字是3602879701896397 x 2⁻⁵⁵,最接近0.2的数字是7205759403792794 x 2⁻⁵⁵;将它们相加会导致10808639105689191 x 2⁻⁵⁵,或精确的十进制值0.3000000000000000444089209850062616169452667236328125。浮点数通常被四舍五入以显示。

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

用三元数,三分之一不是问题——也许一些每只手有15个手指的种族会问为什么你的小数数学坏了……

只是为了好玩,我按照标准C99的定义玩了浮点数的表示,并编写了下面的代码。

该代码打印浮点数在3个分离组中的二进制表示

SIGN EXPONENT FRACTION

之后,它打印一个总和,当总和足够精确时,它将显示硬件中真正存在的值。

因此,当您编写float x = 999...时,编译器会将该数字转换为函数xx打印的位表示,以便函数yy打印的总和等于给定的数字。

实际上,这个总和只是一个近似值。对于数字999,999,999,编译器将在浮点数的位表示中插入数字1,000,000,000

在代码之后,我附加了一个控制台会话,在该会话中,我计算硬件中真正存在的两个常量(减去PI和999999999)的项和,由编译器插入到那里。

#include <stdio.h>#include <limits.h>
voidxx(float *x){unsigned char i = sizeof(*x)*CHAR_BIT-1;do {switch (i) {case 31:printf("sign:");break;case 30:printf("exponent:");break;case 23:printf("fraction:");break;
}char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;printf("%d ", b);} while (i--);printf("\n");}
voidyy(float a){int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));int fraction = ((1<<23)-1)&(*(int*)&a);int exponent = (255&((*(int*)&a)>>23))-127;
printf(sign?"positive" " ( 1+":"negative" " ( 1+");unsigned int i = 1<<22;unsigned int j = 1;do {char b=(fraction&i)!=0;b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);} while (j++, i>>=1);
printf("*2^%d", exponent);printf("\n");}
voidmain(){float x=-3.14;float y=999999999;printf("%lu\n", sizeof(x));xx(&x);xx(&y);yy(x);yy(y);}

这是一个控制台会话,我在其中计算硬件中存在的浮点数的实际值。我使用bc打印主程序输出的总和。可以在pythonrepl或类似的东西中插入该总和。

-- .../terra1/stub@ qemacs f.c-- .../terra1/stub@ gcc f.c-- .../terra1/stub@ ./a.outsign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29-- .../terra1/stub@ bcscale=15( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29999999999.999999446351872

就是这样。999999999的价值实际上是

999999999.999999446351872

您还可以使用bc检查-3.14是否也被扰动。不要忘记在bc中设置scale因子。

显示的总和是硬件内部的总和。您通过计算获得的值取决于您设置的刻度。我确实将scale因子设置为15。从数学上讲,以无限精度,似乎是1,000,000,000。

另一种方式来看待这个:使用64位来表示数字。因此,无法精确表示超过2**64=18,446,744,073,709,551,616个不同的数字。

然而,Math说0和1之间已经有无限多的小数。IEE 754定义了一种编码,以有效地使用这64位来处理更大的数字空间加上NaN和+/-Infinity,因此在精确表示的数字之间存在差距,仅用近似的数字填充。

不幸的是,0.3处于差距之中。

由于这个线程有点分支到对当前浮点实现的一般讨论中,我想补充的是,有一些项目正在解决他们的问题。

https://posithub.org/为例,它展示了一种名为的数字类型,它承诺以更少的位提供更好的准确性(及其前身unum)。如果我的理解是正确的,它也解决了问题中的那种问题。非常有趣的项目,背后的人是一位数学家它约翰·古斯塔夫森博士。整个项目是开源的,在C/C++、Python、Julia和C#(https://hastlayer.com/arithmetics)中有许多实际实现。

总之因为:

浮点数不能精确地表示二进制中的所有小数

所以就像10/3一样,在10进制中精确地不存在(它将是3.33……重复出现),同样地,1/10在二进制中不存在。

那又怎样?怎么处理?有没有变通方法?

为了提供最佳解决方案我可以说我发现了以下方法:

parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3

让我解释一下为什么这是最好的解决方案。正如上面答案中提到的其他人一样,使用准备使用JavaScript toFive()函数来解决问题是一个好主意。但很可能你会遇到一些问题。

想象一下,你要把两个浮点数相加,比如0.20.7,这里是:0.2 + 0.7 = 0.8999999999999999

您的预期结果是0.9,这意味着在这种情况下您需要一个精度为1位的结果。所以你应该使用(0.2 + 0.7).tofixed(1)但是你不能只给to固定()一个特定的参数,因为它取决于给定的数字,例如

0.22 + 0.7 = 0.9199999999999999

在此示例中,您需要2位精度,因此它应该是toFixed(2),那么适合每个给定浮点数的参数应该是什么?

你可能会说让它在每种情况下都是10:

(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"

该死!你打算怎么处理9后面那些不需要的零?现在是时候将其转换为浮动,使其如你所愿:

parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9

现在你找到了解决方案,最好将其作为这样的函数提供:

function floatify(number){return parseFloat((number).toFixed(10));}    

你自己试试:

function floatify(number){return parseFloat((number).toFixed(10));} 
function addUp(){var number1 = +$("#number1").val();var number2 = +$("#number2").val();var unexpectedResult = number1 + number2;var expectedResult = floatify(number1 + number2);$("#unexpectedResult").text(unexpectedResult);$("#expectedResult").text(expectedResult);}addUp();
input{width: 50px;}#expectedResult{color: green;}#unexpectedResult{color: red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script><input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =<p>Expected Result: <span id="expectedResult"></span></p><p>Unexpected Result: <span id="unexpectedResult"></span></p>

你可以这样使用它:

var x = 0.2 + 0.7;floatify(x);  => Result: 0.9

正如W3学校所示,还有另一种解决方案,您可以乘法和除法来解决上述问题:

var x = (0.2 * 10 + 0.1 * 10) / 10;       // x will be 0.3

请记住,(0.2 + 0.1) * 10 / 10根本不起作用,尽管它看起来是一样的!我更喜欢第一个解决方案,因为我可以将其应用为将输入浮点数转换为准确输出浮点数的函数。

仅供参考,乘法也存在同样的问题,例如0.09 * 10返回0.8999999999999999。应用Flotify函数作为解决方法:flotify(0.09 * 10)返回0.9

从Python 3.5开始你可以使用#0函数来测试近似相等:

>>> import math>>> math.isclose(0.1 + 0.2, 0.3)True>>> 0.1 + 0.2 == 0.3False

想象一下,以十进制工作,比如说,8位精度。你检查是否

1/3 + 2 / 3 == 1

并学习返回false。为什么?好吧,作为实数,我们有

1/3=0.3332/3=0.666

在小数点后8位截断,我们得到

0.33333333 + 0.66666666 = 0.99999999

当然,这与1.00000000完全不同于0.00000001


具有固定位数的二进制数的情况完全类似。作为实数,我们有

1/10=0.0001100110011001100…(2进制)

1/5=0.0011001100110011001…(2进制)

如果我们把这些截断到,比如说,7位,那么我们就会得到

0.0001100 + 0.0011001 = 0.0100101

而另一方面,

3/10=0.01001100110011…(2进制)

其中,截断为七位,是0.0100110,而它们正好相差0.0000001


确切的情况稍微微妙一些,因为这些数字通常以科学记数法存储。因此,例如,不是将1/10存储为0.0001100,而是将其存储为类似于1.10011 * 2^-4的内容,这取决于我们为指数和尾数分配了多少位。这会影响您计算的精度。

结果是,由于这些舍入错误,您基本上不想在浮点数上使用==。相反,您可以检查它们的差的绝对值是否小于某个固定的小数字。

十进制数,如0.10.20.3不能准确地用二进制编码的浮点类型表示。0.10.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 false0.1 is 0.100000000000000005551120.2 is 0.200000000000000011102230.1 + 0.2 is 0.300000000000000044408920.3 is 0.299999999999999988897770.3 - (0.1 + 0.2) is -5.55112e-17

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

_Decimal32_Decimal64_Decimal128类型可能在您的系统上可用(例如,海湾合作委员会选定目标上支持它们,但Clangosx上不支持它们)。

这实际上很简单。当你有一个以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/

我刚刚看到这个关于浮点数的有趣问题:

考虑以下结果:

error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))1

我们可以清楚地看到断点时2**53+1-所有正常工作,直到2**53

>>> (2**53) - int(float(2**53))0

在此输入图片描述

这是因为双精度二进制:IEEE 754双精度二进制浮点格式:binary64

双精度浮点格式的维基百科页面:

双精度二进制浮点是PC上常用的格式,尽管其性能和带宽成本高,但由于其范围比单精度浮点更宽。与单精度浮点格式一样,与相同大小的整数格式相比,它在整数上缺乏精度。它通常被简单地称为双精度。IEEE 754标准规定binary64具有:

  • 签名位:1位
  • 指数:11位
  • 显着精度:53位(显式存储52位)

在此输入图片描述

具有给定偏置指数和52位分数的给定64位双精度基准假设的实值为

在此输入图片描述

在此输入图片描述

感谢@a_guest向我指出这一点。

浮点数在硬件级别表示为二进制数的分数(以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.10.1000000000000000055511151231257827021181583404541015625

这比大多数人预期的要多得多,所以Python显示一个四舍五入的值来提高易读性:

>>> 0.10.1

重要的是要理解,实际上这是一种错觉:存储值并不完全是1/10,只是在显示器上存储值被四舍五入。一旦您对这些值执行算术运算,这就变得很明显:

>>> 0.1 + 0.20.30000000000000004

这种行为是机器浮点表示的固有特性:它不是Python中的bug,也不是代码中的bug。您可以在所有其他使用硬件支持计算浮点数的语言中观察到相同类型的行为(尽管某些语言默认情况下不会显示差异,或者在所有显示模式中都不显示差异)。

另一个惊喜是这个固有的。例如,如果您尝试将值2.675四舍五入到小数点后两位,您将得到

>>> round (2.675, 2)2.67

round()基元的留档指示它舍入到离零最近的值。由于十进制小数正好在2.67和2.68之间,您应该期望得到2.68的(二进制近似值)。然而,情况并非如此,因为当十进制小数2.675转换为浮点数时,它由近似值存储,其确切值为:

2.67499999999999982236431605997495353221893310546875

由于近似值比2.68略接近2.67,因此舍入向下。

如果您遇到了将十进制数字向下舍入很重要的情况,您应该使用decimal模块。顺便说一句,decimal模块还提供了一种方便的方法来“查看”存储的任何浮点数的确切值。

>>> 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...>>> sum0.9999999999999999

二进制浮点数的算术有许多这样的惊喜。“0.1”的问题将在下面的“表示错误”部分详细解释。有关此类惊喜的更完整列表,请参阅浮点的危险。

确实没有简单的答案,但是不要过度怀疑浮点数!在Python中,浮点数操作中的错误是由于底层硬件造成的,在大多数机器上,每次操作不超过1/2**53。这对于大多数任务来说是必要的,但你应该记住,这些不是十进制操作,每个浮点数操作都可能会出现新的错误。

尽管存在病态情况,但对于大多数常见用例,您只需将其四舍五入到显示器上所需的小数位数即可获得预期结果。有关浮点数显示方式的精细控制,请参阅字符串格式语法了解str.format()方法的格式规范。

这部分答案详细解释了“0.1”的例子,并展示了如何独自执行此类情况的精确分析。我们假设你熟悉浮点数的二进制表示。

>>> 0.1 + 0.20.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)

在:

J ~ = 2 ** N / 10

记住J正好是53位(所以>=2**52但<2**53),N的最佳可能值是56:

>>> 2 ** 524503599627370496>>> 2 ** 539007199254740992>>> 2 ** 56/107205759403792793

所以56是N的唯一可能值,它为J留下了53位。因此,J的最佳可能值是这个商,四舍五入:

>>> q, r = divmod (2 ** 56, 10)>>> r6

由于进位大于10的一半,因此通过向上舍入获得最佳近似值:

>>> q + 17205759403792794

因此,“IEEE-754双精度”中1/10的最佳近似值是2**56以上,即:

7205759403792794/72057594037927936

请注意,由于向上舍入,结果实际上略大于1/10;如果我们没有向上舍入,商将略小于1/10。但在任何情况下都不是1/10!

所以计算机永远不会“看到”1/10:它看到的是上面给出的精确分数,使用“IEEE-754”中的双精度浮点数的最佳近似值:

>>>. 1 * 2 ** 567205759403792794.0

如果我们将这个分数乘以10**30,我们可以观察到它的强权重小数点后30位的值。

>>> 7205759403792794 * 10 ** 30 // 2 ** 56100000000000000005551115123125L

这意味着存储在计算机中的确切值大约等于十进制值0.100000000000000005551115123125。在Python 2.7和Python 3.1之前的版本中,Python将这些值四舍五入到17个有效小数位,显示“0.10000000000000001”。在当前版本的Python中,显示的值是分数尽可能短的值,同时在转换回二进制时给出完全相同的表示,简单地显示“0.1”。

正常的算术是以10为基数,所以小数代表十进制,百进制等。当你试图用二进制的以2为基数的算术表示一个浮点数时,你正在处理一半,四进制,八进制等。

在硬件中,浮点数存储为整数尾数和指数。尾数表示有效数字。指数类似于科学记数法,但它使用的基数为2而不是10。例如64.0将表示尾数为1,指数为6。0.125将表示尾数为1,指数为-3。

浮点小数必须加2的负幂

0.1b = 0.5d0.01b = 0.25d0.001b = 0.125d0.0001b = 0.0625d0.00001b = 0.03125d

诸如此类。

在处理浮点运算时,通常使用错误增量而不是使用相等运算符。而不是

if(a==b) ...

你会使用

delta = 0.0001; // or some arbitrarily small amountif(a - b > -delta && a - b < delta) ...

浮点数的陷阱是它们看起来像十进制,但它们以二进制工作。

2的唯一质因数是2,而10的质因数是2和5。这样做的结果是,每个可以完全写成二进制分数的数字也可以完全写成十进制分数,但只有可以写成十进制分数的数字子集可以写成二进制分数。

浮点数本质上是具有有限有效位数的二进制分数。如果您超过这些有效位数,则结果将被舍入。

当您在代码中键入文字或调用函数将浮点数解析为字符串时,它需要一个十进制数,并将该十进制数的二进制近似值存储在变量中。

当您打印浮点数或将函数转换为字符串时,它会打印浮点数的十进制近似值。可以将二进制数精确转换为十进制数,但据我所知,没有任何语言在转换为字符串*时默认这样做。一些语言使用固定数量的有效数字,其他语言使用最短的字符串,将“往返”回到相同的浮点值。

*Python确实在将浮点数转换为“decimal. Decimal”时准确转换。这是我所知道的获得浮点数精确十进制等效值的最简单方法。