资讯专栏INFORMATION COLUMN

JavaScript是怎样编码数字的[How numbers are encoded in Java

oysun / 2993人阅读

摘要:译者注规范化就是把小数点放在第一个非零数字的后面总结当指数的范围是十进制分数不是所有的十进制分数都能够非常精确的表示例如和都不能够被精确的表示成二进制浮点数。相同的,也不能被精确表示成一个十进制分数,它大概能被表示成。

在JavaScript中所有的数字都是浮点数,本篇文章将介绍这些浮点数在JavaScript内部是怎样被转为64位二进制的。
我们会特别考虑整数的处理,所以读完本篇之后,你会理解为什么会有以下结果发生:

> 9007199254740992 + 1
9007199254740992
> 9007199254740992 + 2
9007199254740994
1. JavaScript的数字

JavaScript数字全部是浮点数。 根据 IEEE 754标准中的64位二进制(binary64), 也称作双精度规范(double precision)来储存。从命名中可以看出,这些数字将以二进制形式,使用64个字节来存储。这些字节按照以下规则分配:

0 - 51 字节是 分数f(fraction )
52 - 62 字节是 指数(exponent )
63 字节 是 标志位 (sign)
标志位 (s, sign) 指数(e, exponent ) 分数(f, fraction )
(1 bit) (11 bit) (52 bit)
63 62 51
52 0

他们按照以下规则表示一个数字: 如果标志位是0, 表示这个数字为正数,否则为负数。粗略来说,分数f用来表示数字的‘数码’(0-9),指数表示这个数字的‘点’在哪里。接下来我们会使用二进制(虽然这并不是通常的浮点数表示方式)。并用一个%作为前缀来标识。虽然JavaScript数字是以二进制保存的,但输出(打印)时通常是以10进制显示. 接下来的例子,我们也会沿用这一规则。

2. 分数f

下表是一种表示非负浮点数的方法:
尾数 (小数点后面的数,significand 或 mantissa ) 以自然数字的形式保存‘数码’,指数决定需要往左(负指数)或者右(正指数)移多少位。再忽略位数,这个JavaScript数字就是 有理数1.f乘以2p。
译者注: 这里指数用p而不是e来表示是因为e是一个偏移量,第三点会详细说明

比如以下例子:

f = %101, p = 2 Number: %1.101 × 22 = %110.1
f = %101, p = −2 Number: %1.101 × 2−2 = %0.01101
f = 0, p = 0 Number: %1.0 × 20 = %1
2.1 表示一个整数

需要多少位来编码一个整数呢? 尾数共有53个数码,1个在‘点’的前面,52个在后面,如果p=52,我们就有一个53位的自然数,现在的问题是最高位总是为1,也就是说我们不能随便的使用所有的位。要去掉这个限制,我们需要2步,首先. 如果需要最高位是0,第二位是1的53位的数字,将p设置为51,这时分数f最低位变成了‘点’后面的第一个数码,也就是整数0。按照这个规律,直到指数p=0,分数f=0,这就是数字1的编码。

52 51 50 ... 1 0 (bits)
p=52 1 f51 f50 ... f1 f0
p=51 0 1 f51 ... f2 f1 f0=0
... ... ... ... ... ... ... ...
p=0 0 0 0 ... 0 1 f51=0, etc.

其次,对于完整的53位数字,我们还需要表示0,我们将在下一段详细介绍。
需要注意的是,我们可以表示完整的53位整数,因为标志位是另外储存的。

3. 指数e

指数占11位,它可以表示0-2047(211-1), 为了支持负指数,JavaScript使用偏移二进制来编码: 1023表示0,小于它的为负,大于它的为正。这就意味着,减去1023才能得到正常点数字。因此我们之前使用的变量p就等于e-1023,也就是尾数乘以2e-1023
例如:

    %00000000000     0  →  −1023  (最小的数字)
    %01111111111  1023  →      0
    %11111111111  2047  →   1024  (最大的数字)
                         
    %10000000000  1024  →      1
    %01111111110  1022  →     −1 

如果需要一个负数,只需要颠倒一下它的位数,再减一

3.1 特殊的指数

有2个指数是保留位。最小的0,和最大的2047. 指数2047表示无穷大(infinity)和 NaN(非数字)值。IEEE 754标准有很多非数字值, 但是JavaScript把他们都表示为NaN。指数为0时有两个意思。1. 如果分数f也是0,表示这个数字就是0.因为标志位是多带带存储的。所以我们有+0和-0;

然后指数0也可以用来表示非常小的数字(接近0)。此时分数f必须为非0,而且,如果这个数字是由%0.f × 2−1022算出来的,这个表示方式叫做非规范化,而之前我们讨论的表示方式叫规范化。最小的非0正数可以被规范化为: %1.0 × 2−1022。 最大的非规范化数字为: %0.1 × 2−1022, 所以,从规范化到非规范化是过渡是平滑的。
译者注: 规范化就是把小数点放在第一个非零数字的后面

3.2 总结:
(−1)s × %1.f × 2e−1023 normalized, 0 < e < 2047
(−1)s × %0.f × 2e−1022 denormalized, e = 0, f > 0
(−1)s × 0 e = 0, f = 0
NaN e = 2047, f > 0
(−1)s × ∞ (infinity) e = 2047, f = 0

当p = e − 1023, 指数的范围是−1023 < p < 1024

4. 十进制分数

不是所有的十进制分数都能够非常精确的表示, 例如:

> 0.1 + 0.2
0.30000000000000004

0.1和0.2都不能够被精确的表示成二进制浮点数。但是这个偏差通常非常非常小,小到不能够被表示出来,加法可以使这个偏差变得可见:

> 0.1 + 1 - 1
0.10000000000000009

表示0.1相当于表示一个分数110,难的部分在于分母是10,10素数分解是2*5. 而指数只能分解2,所以没有办法得到5。相同的, 1/3也不能被精确表示成一个十进制分数,它大概能被表示成0.333333。
但相对的。要用十进制表示一个2进制分数却是永远可行的,值需要使用足够的2(每个10都有1个2)。

%0.001 = 1/8 = 1/2 × 2 × 2 = 5 × 5 × 5/(2×5) × (2×5) × (2×5) = 125/10 × 10 × 10 = 0.125
4.1 对比十进制分数

因此,当你要处理10进制分数,不要直接去比较他们,先想一想,它可能会有一个上限,比如有一个上限叫做机器最小数 machine epsilon. 标准的双精度数的最小数为 2−53.

var epsEqu = function () { // IIFE, keeps EPSILON private
    var EPSILON = Math.pow(2, -53);
    return function epsEqu(x, y) {
        return Math.abs(x - y) < EPSILON;
    };
}();

这个方法可以修正你的比较结果

> 0.1 + 0.2 === 0.3
false
> epsEqu(0.1+0.2, 0.3)
true
5. 最大的整数

“x 是最大的整数”这句话是什么意思呢?它的意思是说,任意整数n在 0 ≤ n ≤ x 范围内都是可以被表示的。也就是说如果大于x,将无法表示。比如253 。任何比它小的数字都可以被表示。

> Math.pow(2, 53)
9007199254740992
> Math.pow(2, 53) - 1
9007199254740991
> Math.pow(2, 53) - 2
9007199254740990
但比它大的就不行
> Math.pow(2, 53) + 1
9007199254740992

关于253 这个上限,有一些很令人惊奇的表现。我们将用一些问题来解释这些现象。你要记住的是,这个上限是分数f的上限,指数e部分其实还有空间。

为什么是53位呢?你有53位来表示数的大小,除去标志位。但是分数f却是由52位组成的,这是为什么呢。从前面的文章可以看出,指数e从第53位开始,它会移动分数f,所以这个53位的数字(除了0)可以被表示出来,并且有一个特别的数字去表示0(并且分数f也是0).

为什么最大的数不是253−1? 通常来说,x位就说明最小数是0,最大值是2x−1. 比如8位数字最大是255。而在JavaScript里,最大的分数f确实是253−1,但253 也可以被表示出来,因为有指数e的帮助。它只要让分数f等于0,指数e等于53即可。

%1.f × 2p = %1.0 × 253 = 253

为什么大于253就不能表示了呢?例如:

> Math.pow(2, 53)
9007199254740992
> Math.pow(2, 53) + 1  // not OK
9007199254740992
> Math.pow(2, 53) + 2  // OK
9007199254740994
> Math.pow(2, 53) * 2  // OK
18014398509481984

253×2 可以表示正确,因为指数e还可以用,乘以2仅仅需要指数e加一,而不影响分数f。所以乘以2的幂不是问题,只要分数f没有超过上限,那为什么2加253也可以表示正确,1却不可以呢,我们扩大一下之前的,加上53 和54位来看看。

54 53 52 51 50 ... 2 1 0 (bits)
p=54 1 f51 f50 f49 f48 ... f0 0 0
p=53 1 f51 f50 f49 ... f1 f0 0
p=52 1 f51 f50 ... f2 f1 f0

看p=53的那一行,它应该是一个JavaScript数字,53位设置成了1,但是因为它的分数f只有52位,而0位必须位0,而只有253 ≤ x < 254中的偶数数字x可以被表示。在p=54时,这个空间增加到乘以4,在 254 ≤ x < 255: 中。

> Math.pow(2, 54)
18014398509481984
> Math.pow(2, 54) + 1
18014398509481984
> Math.pow(2, 54) + 2
18014398509481984
> Math.pow(2, 54) + 3
18014398509481988
> Math.pow(2, 54) + 4
18014398509481988
6. IEEE 754 的例外

IEEE 754标准描述了5中例外 , 当出现这些例外,就无法算出准确的数字。

1. 无效 : 进行一个无效操作。例如,给一个负数开平方,返回NaN

> Math.sqrt(-1)
NaN

2. 除以0 : 返回正或者负的infinity(无穷大)

> 3 / 0
Infinity
> -5 / 0
-Infinity

3. 溢出(overflow) : 结果太大,无法表示。这时是指数已经太大, (p ≥ 1024).根据标志位,正或者负溢出,返回正或者负的infinity(无穷大)。

> Math.pow(2, 2048)
Infinity
> -Math.pow(2, 2048)
-Infinity

4. 潜流(underflow): 结果太接近于0,这时是指数已经太小(p ≤ −1023). 返回一个非规范化的数字,或者0.

> Math.pow(2, -2048)
0

5. 不精确(Inexact): 一个操作返回不精确的结果 - 有太多有意义的数字需要分数f去存,那就返回一个四舍五入的结果

> 0.1 + 0.2
0.30000000000000004
    
> 9007199254740992 + 1
9007199254740992

上面的第三点和第四点是关于指数的,第五点是关于分数f的,第三点和第五点的差别非常小,第五点的第二个例子,我们已经接近了分数f的最大值(这也可以算是一个溢出操作)。但根据 IEEE 754只有超过了指数的范围才算溢出。

7. 结论

本篇文章中,我们观察了JavaScript是怎样把浮点数存进64位中的。它之所以这么做是根据 IEEE 754 标准中的双精度。因为我们常常忘记,JavaScript对于分母质因分解不仅包含2的数字 是无法精确表示的。比如0.5(1/2),是可以精确表示的,但0.6(3/5)就不能。我们很容易忘记一个整数是由标志位,分数f,指数3部分组成,然后就会面对Math.pow(2, 53) + 2 可以计算正确,而Math.pow(2, 53) + 1会计算错误的问题。

8. 资源和引用

• “IEEE Standard 754 Floating-Point” - Steve Hollasch.
• “Data Types and Scaling (Fixed-Point Blockset)” in the MATLAB documentation.
• “IEEE 754-2008” on Wikipedia

本文也同时是JavaScript 数字系列 , 它包含:

JavaScript中的数字显示

JavaScript中的NaN 和 Infinity

JavaScript的两种0

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108771.html

相关文章

  • js 中 number 为何很怪异

    摘要:但这个数值并不安全从到中间的数字并不连续,而是离散的。对象中的常量表示与可表示的大于的最小的浮点数之间的差值。绝对值的最大安全值。寻找奇怪现象的原因为什么结果是与的逼近算法类似。 js 中的 number 为何很怪异 声明:需要读者对二进制有一定的了解 对于 JavaScript 开发者来说,或多或少都遇到过 js 在处理数字上的奇怪现象,比如: > 0.1 + 0.2 0.30000...

    MRZYD 评论0 收藏0
  • 图解:JavaScript中Number一些表示上/下限

    摘要:例如指数实际值为,在单精度浮点数中的指数域编码值为,即采用指数的实际值加上固定的偏移值的办法表示浮点数的指数,好处是可以用长度为个比特的无符号整数来表示所有的指数取值,这使得两个浮点数的指数大小的比较更为容易。 自己整理、设计的,转载请注明原帖。先从这个demo看起:http://alvarto.github.io/Visu... 数轴 showImg(http://segmentfa...

    SillyMonkey 评论0 收藏0

发表评论

0条评论

oysun

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<