math - 浮点数哪里broken了?

  显示原文与译文双语对照的内容

0.1 + 0.2 == 0.3
-> false


0.1 + 0.2
-> 0.30000000000000004

为什么发生这种情况?

时间:

二进制浮点算术像这样。 在大多数编程语言中,它都是基于 754标准 。 JavaScript使用 64位 浮点表示,它与java的double 相同。 问题的关键是数字以一个整数乘以一个整数来表示;有理数( 比如 0.1,它是 1/10 )的分母不是两个幂的幂不能精确表示。

对于标准的binary64 格式中 0.1,表示可以写入严格按照

  • 0.1000000000000000055511151231257827021181583404541015625 十进制,或者
  • 0x1.999999999999ap-4 中的( c hexfloat表示法) 。

相反,有理数 0.1,即 1/10,可以完全写成

  • 0.1 以十进制,或者
  • 0x1.99999999999999...p-4 在一个模拟的C99 hexfloat表示法中,其中 ... 代表了 9的无穷序列

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

一个相当复杂的讨论Floating-Point算术问题是面向 每个计算机专家应该知道什么Floating-Point算术 。 有关easier-to-digest解释,请参见 floating-point-gui.de

设计师 硬件角度看,

我相信我应该在设计和构建浮点硬件之后添加设计器的硬件透视图。 了解软件中的错误可能有助于理解发生什么事的起源,最终,我希望这可以帮助你了解对为什么浮点误差的原因发生这种情况,似乎随时间积累。

1.概述

在最后place,从工程的角度,大多数会有一些元素的数据类型错误,因为硬件可以进行浮点运算操作所需的浮点计算中只有一个误差小于1的一半的一个 unit. 因此,很多硬件将停止在精度上,只是需要产量不少于二分之一的单个操作是在浮点数除法尤其成问题的最后一个地方一个单位错误。 单个操作的构成取决于该单元所占用的操作数数。 最多是两个,但有些单位接受 3个或者更多的操作数。 正因为如此,无法保证重复操作将导致理想错误,因为这些错误加起来随着时间。

2.标准

大多数处理器遵循 IEEE-754 标准,但有些处理器使用非规范化或者不同标准。 precision,为代价,例如有一个非常小的浮点 numbers. IEEE-754它允许表达中不正常的模式, 下不过,将涵盖了归一化的IEEE-754模式这是典型的操作模式。

于中的一个operation,相关标准,硬件设计人员被允许任何错误/ε,只要它的值小于IEEE-754半边只在最后一个位置,一种单位,结果必须要小于1的一个单位在最后place.的一半 这解释了为什么当重复操作发生时,错误会增加。 对于IEEE-754双重精度,这是 54了位,因为 53 ( 规范化化) ( 也称为尾数,浮点数的位都是用来表示数值的部分( 例如 5.3中的5.3 ( e5 ) 。 下一节将更详细地介绍各种浮点操作中硬件错误的原因。

3 。除法中舍入错误的原因

浮点除法错误的主要原因,是用来计算商的除法算法。 大多数计算机系统使用乘法来计算除法,主要在 z=x/y,Z = X * ( 1/y ) 中。 在最后place,除法计算均以迭代方式 换句话说,在计算过程后有一些商直至达到要求精度时,它将每个周期小于一 unit. IEEE-754是什么( 出现错误), 在中商( QST ) 选用表表Y ( 1/y ) 互为倒数,称为慢划分,且通常将在商选择表的大小( 以位为单位),或者的编码位数的进制商每次迭代中计算的宽度,但是加上了一些警戒位。 标准,双精度( 64位 ),它的大小就会为IEEE-754分隔符(,但是加上了一些守卫的的进制位k,其中k> =2. 因此,例如一个典型的为一个分频器来计算商选择表 2比特的商一次( 基数 4 ) 将是 2 +2= ( 加上一些可选的位) 4位。

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

商选择表中的倒数取决于除法方法: 为了试图产生最小可能的error,/如可疑交易报告部门,或者快或者慢/如Goldschmidt部门;每个条目被修改根据除法算法的人脸识别 不过在任何情况下,所有的倒数是 近似值的实际的倒数,和引入某些元素的错误。 同时在最后place,慢除法和商除法的方法是直接计算的迭代式地,换句话说,快一些的编码位数商进行了计算,那么结果从被除数中减去除数,每个步骤与在分片重复该步骤,直到误差小于一个一半的一个单元组成 缓慢的界定方法计算固定位数的数字中商的每一个步骤,并常常比较便宜去构建,并且每个步骤和快速除法的方法是直接计算一个变量的位数通常是更经济地构建。 最重要的是,大多数故障都依靠重复的乘法由一个分区的一部分方法进行逼近互惠的,所以他们做很容易出错。

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

所有操作中舍入错误的另一个原因是IEEE-754允许的最终应答的不同模式。 有截断,round-towards-zero,round-to-nearest ( 默认), round-down和 round-up 。 在最后一个位置,让单个 operation, unit.介绍元素误差小于1的所有方法一半的一个 随着时间和重复操作,截断也会向结果错误中添加累积。 这个截断错误,尤其是幂的问题,它涉及到某种形式的重复乘法。

5.重复操作

因为硬件,即是否小于1的浮点计算只需要产生一个结果( 出现错误) 一个单位在最后一个位置,让单个操作的一半,而错误将增长重复操作如果没有监视了。 它的原因就是这是在计算量需要有大小限制的错误,数学家的使用方法,如利用 round-to-nearest 即使位在 IEEE-754,最后的选择,因为随着时间的推移,这些错误都会更有可能相互抵消,和区间运算结合变化的硕士论文 754舍入模式来预测舍入错误,并加以改正。 由于与其他舍入模式相比,它相对较低,舍入到最近的偶数位( 在最后一个地方),是IEEE-754的默认舍入模式。

于一个operation,相关请注意,默认舍入模式,round-to-nearest 甚至小于1的数字在最后的选择,保证了一个错误在最后place.一种单位的一半 利用截尾,round-up以及单独环绕下来可能会导致发生错误,是大于1的一半是一个单位中在最后一个位置,而是小于一个单位的最后一个地方,所以这些模式是不推荐,除非它们是用在区间运算。

6.摘要

总之,采用硬件浮点计算中的几何本质是错误的组合是截断,并在的情况下截断中的相互分裂。 自从IEEE-754标准中只规定一个误差少于一半的一个单位在最后对于单个操作的地方加起来,通过重复操作将浮点错误,除非纠正。

浮点舍入错误。0.1在base-2中不能像base-10那样精确表示,因为缺少 5的主要因子。 就像 1/3 使用无限位数表示十进制,但在base-3中是" 0.1",0.1在base-2中使用无限位数,在中它不在base-10中。 计算机没有无限的内存。

将. 1或者 1/10 转换为 base 2 ( 二进制) 后,在小数点后得到重复图案,就像在 base 10中表示 1/3 一样。 值不准确,因此你不能使用普通浮点方法来精确地计算数学。

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

想象你正在试图切开比萨饼。 你有一个可以切割比萨切片的机器比萨刀,一半。 它可以使整个比萨减半,或者它可以减半,但在任何情况下,减半总是精确的。

于连起high-precision相关abilities,这比萨饼刀拥有非常精细的动作,如果你开始时一整个比萨,然后每次对分这一点,并继续中分最小的分片,你可以做在二分解 53次切片之前,太小的, 此时,你不能再减半的切片,但必须包括或者排除它。

所有这样的方式这将购买高达中的切片,你会如何,另一条 one-tenth ( 0.1 ) 或者( 0.2 ) 五分之一的一个比萨饼? 好好考虑一下,然后尝试一下。 你甚至可以尝试使用真正的比萨,如果你手头有一个神话般的精密比萨切割器。 :- )


大多数有经验的程序员,当然,知道真正的答案,它就是没有办法拼凑一个精确十或者五分之一的比萨使用这些切片,不管你多细碎砍他们。 你可以做一个相当好的近似,如果你把 0.1的近似值加上 0.2,你得到了 0.3的相当好的近似值,但它仍然是一个近似的近似值。

对于double-precision数字( 这就是让你的比萨饼减半的精确度),立即小于 0.1的数字是 0.099 99999999999999167332731531132594682276248931884765625和 0.100 0000000000000055511151231257827021181583404541015625. 后者比前者更接近 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.100 0000000000000055511151231257827021181583404541015625 + 0.200 000000000000011102230246251565404236316680908203125 = 0.300 0000000000000444089209850062616169452667236328125,而最靠近 0.3数实际上是 0.299 999999999999988897769753748434595763683319091796875.


P.S 。某些编程语言还提供比萨刀,它可以切片分割成精确的第十个 。 虽然这种比萨饼刀不常见,如果你有权限看到一个,你应该使用它时,重要的是要能够得到完全one-tenth或者一个切片的五分之一。

( 最初张贴在quora上。)

除了其他正确的答案之外,你可能需要考虑扩展你的值以避免floating-point算法的问题。

例如:


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,但幸运的是floating-point中的整数算术是精确的,所以可以通过缩放来避免十进制表示错误。

作为一个实际的例子,为了避免floating-point问题在精确度非常高的地方,建议使用 1 要将货币作为代表百分比的整数来处理: 2550 美分而不是 25.50 美元。


1 道格拉斯 Crockford: 的JavaScript: 好的部件: 附录A - 难看的部分( 第 105页) 。

解决难看的溢出问题的解决方案


function strip(number) {
 return (parseFloat(number.toPrecision(12)));
}

使用'toPrecision(12)'留下的尾随零删除'parseFloat()'。 假设在最小有效数字上加/减一个是正确的。

我的解决方法:


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

精度是指在添加小数点后要保留的位数。

我的答案很长,所以我把它分成三个部分。 因为这个问题是关于浮点数学,我把重点放在机器实际上是什么。 我还把它指定为双精度精度,但是这个参数同样适用于任何浮点算术。

前导

754 double-precision floating-point格式( binary64 ) 数字代表了一个形式

值= ( -1 ) ^s * (-1.m ) 51 m 50 - m 2 m 1 m 0 ) 2 * 2 e-1023

在 64位中:

  • 第一个位是符号位: 1 如果数字为负数,则为 01
  • 下一个是指数,这是由 1023 偏移 12位。 换句话说,从double-precision数字读取指数位之后,1023必须被减去以获得两个。
  • 其余 52位是 significand ( 或者尾数) 。 在尾数中,'隐含的'1. 总是 2 因为任何二进制值的最重要位是 1

1 - 硕士论文 754允许为一个有符号零 - +0的概念和 -0 处理方式不同: 1/(+0) 为正无穷大;1/(-0) 为负无穷大。

2 - 对于 denormal数字,偏移量指数为零。 denormal双精度数字的范围为d 最小值 |x| ≤ 最大 ,在那里, 最小值 ( 最小可以表示非零数) 为 2 -1023 - 51 ( ≈ 4.94 * 10 -324 ) 和d 最大 ( 最大的denormal数,尾数由 1 组成) 是 2 -1023 + 1 - 2 -1023 - 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版本)

@CatoJohnston ( 问题 asker ) 询问为什么 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 可以表示精确到 十进制数字。

将指数转换为十进制,删除偏移量,以及re-adding隐含的1 ( 方括号中),0.1和 0.2:


0.1 = 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 = 2^-3 * [1].1001100110011001100110011001100110011001100110011010

要添加两个数字,指数需要相同,i.e.:


0.1 = 2^-3 * 0.1100110011001100110011001100110011001100110011001101(0)
0.2 = 2^-3 * 1.1001100110011001100110011001100110011001100110011010
sum = 2^-3 * 10.0110011001100110011001100110011001100110011001100111

因为总和不是表单 2 n * 1.{bbb} 将指数逐个增加,并将小数( 二 ) 点移动到:


sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)

从0 到 52 bits,中现在有 53位尾数( 53在上方的方括号中),因此最后位是舍入 away:


sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100

磅;DR博士

0.1 + 0.2 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 - 与原始值相比不重要的( 对于许多应用程序) 。

比较浮点数的最后一小部分是危险的,因为读取著名的"每个计算机科学家都应该知道Floating-Point算术"( 涵盖这个答案的所有主要部分)的任何人都会知道。

大多数计算器使用额外的保护数字来绕过这个问题,这就是 0.1 + 0.2 如何给出 0.3: 最后的一小部分是圆形的。

浮点舍入错误。从中,每个计算机科学家都应该知道Floating-Point算术值:

将无限多实数压缩成有限的位数需要一个近似的表示。 尽管有无穷多整数,但在大多数程序中,整数计算的结果可以存储在 32位。 相反,给定任意数量的位数,实数的大多数计算都会产生数量不能精确表示的数量。 因此,floating-point计算的结果必须经常被舍入,以适应它的有限表示。 舍入误差是floating-point计算的特征特征。

...