每一个Javascript程序员都应该了解的浮点知识

来源:互联网 发布:阿里云实例重装系统 编辑:程序博客网 时间:2024/05/18 14:12


在Javascript开发人员的职业生涯中会碰到一些奇怪的错误——似乎基本的数学运算行不通。在这样或那样的时候,有人会告诉你实际上Javascript中的数字就是浮点。任何人试图去理解浮点或要搞清楚它们为什么如此奇怪,最终都会去看一篇长而难懂的文章。而我这篇文章想用简单的方式让Javascript开发人员理解浮点。

这篇文章假定读者熟悉二进制与十进制间的转换(例如:1写成 12写成10b,3是11b,4是100b...等等)。去澄清更多的事情,在这篇文章中,词语“小数”主要是指机器中数字的十进制表示(例如:2.718)。词语“二进制”在这篇文章中指的是机器表示法。将会分别注明"基于十进制"或"基于二进制"。


浮点数表达方式(以下简称浮点表示法)

去弄懂什么是浮点表示法,我们首先想到的是有非常多种类的数字,我们将逐步讲解。我们把1叫做整数——它是没有小数的整数。

1/2 叫做分数。它意味着整数1被分隔成两部分。这个分数的概念是非常重要的,用它来得到浮点表示法。

通常0.5被认为是一个小数。但是,一个非常重要的区别需要说明——实事上十进制小数0.5代表分数1/2 ,0.51/2十进制表示方式——在这篇文章中,我们叫它点记法。我们把0.5叫有限小数,因为小数点后面的部分是有限的——在0.5的数字5后面没有更多数字。当1/3用小数表示时就是一个无限小数,例如:0.3333....。再次强调下,这个概念在下面的讨论中非常重要。

这还有另一种方式来表示整数、分数和小数。你可能从来没见过这种方式。它大概是这样的:6.022 x 1023(提示:这是一个阿佛加德罗常数,一个化学分子式)。它通常被认为是标准表达式或科学表达式。这种表达式可以推算到这样:
D1.D2D3D4...Dp x BE
这种形式叫做浮点表示法

数字 p 跟着D,D1.D2D3D4...Dp叫有效数字或尾数。p 是有效数字的数量,通常叫做精度。x 跟随在尾数后面,它是表达式的一部分(在这篇文章中乘法符号用*表示)。基数后面紧随着的是指数。指数可以是正数或负数。

浮点表示法之美在于它可以完全的表示任意数。例如:整数1可以用1.0 x 100表示,光速表示为2.99792458 x 108米每秒, 1/2可以用2进制表示:0.1 x 20


移除小数点

在上面的实例中,我们仍然很难完全弄懂小数(数字中有小数点)。当用二进制表示小数时会出现一些问题。给出一个任意的浮点数,例如圆周率π,我们可以用浮点表示法为:3.14159 x 100。用二进制表示它是这样的: 11.00100100 001111....。假设这个数用16位方式去表示,这个数字在机器中会是这样:11001001000011111。现在问题是:小数点去哪了?这个尽然不包含指数(我们假定这是基于二进制的)。
如果数字是5.14159呢?整数部分应该是101 ,代替上面的11,需要多一个位字段。当然我们可以指定第一个N位的字段属于整数部分(例如:小数点左边的部分),剩下的属于小数部分,不过这个知识点属于另一篇主题为定点数的文章。

一旦我们移除小数点,有两个东西可以利用:指数和尾数。我们可以通过公式转换来移除小数点,推算到浮点表示法:
D1D2D3D4...Dp / (Bp-1) x BE
这个格式是我们刚才得到的二进制浮点表示法。注意到现在有效数字是一个整数。这样做能让机器非常简单的存储浮点数。事实上,非常广泛的二进制浮点表达式是IEEE754标准。

IEEE754

Javascript中的浮点表达式格式指定为IEEE754。特别说明下,它是一个双精度格式,意思是给每一个浮点分配64位空间。虽然IEEE754不是唯一的二进制浮点表达式,但是它是使用最广泛的格式。在64位的二进制中它的格式如下:

可能要注意到浮点数机器表达式和我们写的表达式有一点点区别——这是惯例。在64位可用空间中,有一位用来标记——不管这个数是正数还是负数。11位用来放指数——这个允记的最大指数1024。剩下的52位分配给尾数。也许你一直奇怪在javascript中怎么会有 +0 和 -0 ,用符号位解释——所有的数字在Javascript中都有符号位。 Infinity  NaN  也是用浮点表达式编码的——带有一个 2047 的特殊指数。如果尾数是 0,它是一个正整数或负整数。如果不是,那么它是 NaN


舍入误差

上面浮点表达式已经介绍完毕,现在进入一个更棘手的问题——舍入误差。这是所有程序员编写浮点数相关代码时的痛苦来源,Javascript更是如此,因为在Javascript中只能用数字格式表示浮点数。

上面已经提到分数不能完全用十进制表示,例如:1/3。这个问题普遍存在。例如,在二进制中,1/10不能完全表示出来,它可以表示为 0.00110011001100110011...注意到这个0011 是无限循环的。这种特殊情况是因为发生了舍入误差。

首先,初步了解一下舍入误差。来看这个非常著名的无理数,派: 3.141592653589793...。非常多人记住了5个数(3.1415),这就是舍入的很好解释,下面我们会用到这个解释。舍入误差可以这样计算:

(R - A) / Bp-1

这里面 R 是取整数, A 是实数。 B 和我们先前提到的 p相同,它是表示精确度。所以这个难忘的派有了一个舍入误差: 0.00009265...

这个问题似乎不严重,让我们试试二进制数。用分数1/10。基于10进制,它写成0.1。用二进制它是:0.0011001100110011...。假定我们舍入到5个尾数,它是 0.0001。但是 0.0001在二进制中实际上是1/16(或者0.0625)!这意味这有一个0.0375的舍入误差,这个误差相当大。想象一下做一个基本的数学运算0.1+0.2,结果确是0.2625!


幸运的是,按照浮点表示法的规定,ECMAScrip指定了52位尾数,所以这个舍入误差非常小——这个规定特别详细说明了大部分数字的预计舍入误差。因为随着时间的推移执行浮点运算操作引发的错误会越来越多,IEEE 754标准也为数学运算指定了算法。


除了这些还有一点要注意。结合性的算法在处理浮点时不能保证正确,即使在高精度情况下。我的意思是 ((x + y) + a + b) 不一定等于 ((x + y) + (a + b))


而且这是Javascript程序员的痛苦来源。例如,在Javascipt中,0.1+0.2===0.3将得到false。希望你现在能理解这是为什么。当然还有更糟糕,事实上舍入误差在每一个连续的数学运算中都存在。

在JavaScript中处理浮点
在用Javascript处理数字方面已经有了大量的建议,有好的有坏的。大量的建议是在Javascript做算术运算前或后取整数。
在少数的建议中我见到过将所有数存成整数(不是类型)来操作,然后格式化去显示。例如:总价用分做单位。这有一个显然的问题——在这个世界上不是所有的国家的货币是小数(例如:毛里求斯币)。同时,有些国家的货币没有子单位(例如:日本),有些不是100的子单位(例如:阿拉伯币),或者多于一个子单位(例如:中国人民币)。最终,你只能再利用浮点——可能很差劲。

我见过最好的建议是用一些充分测试过的代码库去处理浮点,例如: sinfuljs 或者 mathjs我个人更喜欢mathjs(但是实际上,对于任何数学运算相关的计算我尽量远离Javascript)。BigDecimal 也特别好用,用来处理任意精度数学运算。

另一个常见的建议是使用内置函数 toPrecision()  toFixed()方法。任何想去使用它的人请注意——这些方法返回字符串格式。你可以看看下面的代码:

function foo(x, y) {    return x.toPrecision() + y.toPrecision()}> foo(0.1, 0.2)"0.10.2"

内置函数 toPrecision()  toFixed()方法目的是为了显示。小心使用。


总结

Javascript里的数字是真正的浮点。由于用二进制表示数字的不足,同时机器性能有限,我们只能得到有舍入误差的数据。这篇文章解释了舍入误差和为什么会有这种情况。总是使用这一个好的代码库代替自己去编写代码。




原文: http://flippinawesome.org/2014/02/17/what-every-javascript-developer-should-know-about-floating-points/
0 0