6

JavaScript 中数字的底层表示

 5 years ago
source link: https://harttle.land/2018/06/29/javascript-numbers.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

至今 JavaScript 已经有6 种基本类型 了,其中数字类型(Number)是表示数字的唯一方法。 目前其标准维护在 ECMA262 ,在 JavaScript 语言层面不区分整数与浮点、符号与无符号。 所有数字都是 Number 类型,统一应用浮点数算术。 由于 JavaScript 中无法访问低层的二进制表示,而且 64 位可表示范围非常大,不容易遇到和了解到边界情况。 这篇文章对 JavaScript Number 的二进制表示进行简要的介绍,主要明确使用者观察到的边界, 解释 MAX_VALUE , MIN_VALUE , MAX_SAFE_INTEGER , MIN_SAFE_INTEGER , EPSILON 这些常量取值的原因;回答 POSITIVE_INFINITY , NEGATIVE_INFINITY , NaN 这些常量的表示方法。

二进制表示

JavaScript 数字占 64 位,与 C++ 中的 double 类型一样,采用 IEEE 754 规范的 双精度浮点数 。 字节分配如下图,从高位到低位依次是一个符号位(sign)、11个指数位(exponent)、52个分数位(fraction)。

j2IvY3m.png!web

这样一个数字它的值等于:

为方便讨论,下文使用简写:

为了表示负的指数,指数部分存在一个偏移量 -1023 。 对于非零值第一位有效位始终为 1,因此二进制表示中省略了这个 1,分数位分别表示 1/2, 1/4,1/8,…。 上式表示的浮点数称为 normal number , 特殊值( 0 , NaN , Infinity )和 subnormal number 不同于上述公式, 见下文。

normal number

语法:指数部分 $1 \leq e \leq 2046$ 的值会被解析为 normal number 。 0 和 2047 分别被用于 subnormal number 和特殊值,见 下文

概念: normal number 只表示非零值, 并规定省略第一个非零的 1 ,significant(有效位数)部分可以多一位精度。 指数部分的最大值为 2046,因此 normal number 的最大值 (也是 Number 的最大值Number.MAX_VALUE 的值)为:

最大最小值

这个最大值略小于 $2^{1024}$,相差 $2^{971}$。 Math.EPSILON 表示大于 1 的最小浮点数与 1 的差, 它的值等于:

normal number 的最小值(也是 Number 的最小值Number.MIN_VALUE 的值) 为上述最大值的负值,只变化符号位:

normal number 的指数部分最小值为 1, fraction 部分最小为 0,significant(有效位数)部分最小为 1, 因此 normal number 能够表达的最小正数为:

注意后四位小数是 2014 哈哈,normal number 能够表达的最大负数也是上面的大小,符号位变负。

整数的表示

此外,所有非零整数都属于 normal number,它们的指数部分刚好能够把所有分数移出到小数点左侧,数学地表示为:

其中 $l$ 为最后一个非零的分数下标,第一位分数 $l = 1$。 最大的安全整数 (即 Number.MAX_SAFE_INTEGER 的值)为:

反之, 最小的安全整数 (即 Number.MIN_SAFE_INTEGER 的值)为:

之所以称为最大安全整数,是因为它的每一位都未被四舍五入 (JavaScript Number 实现的浮点数 Rounding 策略是 Round to nearest, ties to even )。 这意味着这个整数是准确的,再大就不准了,例如:

console.log(Number.MAX_SAFE_INTEGER);
// sign=0, fraction=9007199254740991(52个1), e=1023+52
// 输出 9007199254740991

console.log(Number.MAX_SAFE_INTEGER+1);
// sign=0, fraction=0(全0), e=1023+53
// 输出 9007199254740992,这个值是准确的,但它对应两个整数:2^53, 2^53+1,见下一个例子

console.log(Number.MAX_SAFE_INTEGER+2);
// sign=0, fraction=0(全0),e=1023+53
// 输出 9007199254740992

Number.MAX_SAFE_INTEGER+2 是一个奇数,因为缺少一个二进制位不存在精确表示。 可选 fraction=1(最低位为1),或 fraction=0(全0),根据 ties to even 策略,选择 0 让值变成偶数。 与 ties to even 对应的还有 ties to odd 策略,为什么不用四舍五入呢? 因为这样 5 总是“入”的,在累加时会放大误差;绑定到最近的奇数或偶数则会两两抵消,避免误差放大。

subnormal number

语法:指数部分 $e = 0$,且分数部分 $fraction \neq 0$ 的值会被解析为 subnormal number

概念:normal number 省略前导的 1 虽然能够多一位有效位数, 但首位有效数字必须为 1 也限制了最小正数的大小。 subnormal number 就是来弥补 0 与 1 之间的取值的。 它规定指数部分全零且没有前导 1,但计算时采用 -1022 作为指数(等效 e=1), 一个 subnormal number 的值由以下公式给出:

因此 最大的 subnormal number 为:

它与最小的正 normal number($2^{-1022}$)相差 $2^{-1074}$。 最小的 subnormal number 与它大小相同,符号为负。

最小的正 subnormal number(也是 Number 能够表示的最接近 0 的数值),它的值为:

spetial values

0

语法:指数部分 $e = 0$,且分数部分 $fraction = 0$ 的值会被解析为零。

概念:根据 $sign$ 的取值,有 $+0$, $-0$ 两种 0 的表示。

Infinity

语法:指数部分 $e = 11111111111_2$,且分数部分 $fraction = 0$ 的值会被解析为 $\infty$。

概念:根据 $sign$ 的取值,有 Number.NEGATIVE_INFINITY , Number.POSITIVE_INFINITY 两个值。

NaN

语法:指数部分 $e = 11111111111_2$,且分数部分 $fraction \neq 0$ 的值会被解析为 NaN

概念:由于不限定分数部分取值, NaN 值有很多种表示。

符号位可以取任意值($2$ 种),分数只是不可取零($2^{52} - 1$), 因此共有 $(2^{52}-1)\times 2 = 2^{53} - 2$ 种。

一些讨论

Number 一共多少种值?

Number 使用64位双精度浮点数实现,根据指数部分的值来区分不同的表示法。

  • 指数为 0
    • 分数为 0 表示 0,正负共 $2$ 个
    • 分数不为 0 表示 subnormal numbers,共 $(2^{52}-1)\times 2 = 2^{53} - 2$ 个
  • 指数为 2047(全1)
    NaN
    
  • 指数为其他值,表示 normal numbers,共 $2 \times 2^{52} \times (2^{11}-2) = 2^{64} - 2^{54}$ 个

加起来共有 $2^{64}$ 种值(当然 64 位嘛),减去重复的 NaN ($\pm 0$ 是不重复的,它们作除数时会分别得到 $\pm \infty$), Number 能够表示的不重复的值 有:

Number 精度到底如何?

所以 浮点数的精度如何 呢?精度取决于连续两个双精度浮点数之间的差,这个差取决于指数的大小。

  • 对于 normal number(绝对值大于等于 $2^{-1022}$)来讲,指数越大(通常数字越大)精度越小,1 附近的精度由 Number.EPSILON 给出(见上文);
  • 对于 subnormal number(绝对值小于 $2^{-1022}$)来讲,指数是固定的,精度是确定的 $2^{-1074}$;

Number 转换为 32 位整数

虽然 Number 都适用浮点数运算(Floating point arithmetic),但有些运算符和方法只支持 32 位整数。 这时会进行JavaScript 类型转换, 这于 << , >> , >>> , | , & , parseInt() , Atomics.wait() 等操作, 会先调用 ToInt32 转换类型:把 64 位 Number 先转换为整数(abs 后再 floor), 再取其低 32 位作有符号 32 位整数解释(即第一位被当做符号位,以 two’s complement 解释)。 例如:

console.log(Math.pow(2, 100) << 2)

输出为 0,因为 $2^{100}$ 的低 32 位全零,解释后的结果为 0,左移一位仍然为 0。

console.log((Math.pow(2,50) - 1) << 1)

输出为 -2,因为 $2^{50} - 1$ 的低 32 位全1,解释后的结果为 -1,左移一位右侧补 0 得到 -2。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK