Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Javascript 进阶之浮点数储存 #15

Open
Ziphwy opened this issue Jan 26, 2021 · 0 comments
Open

Javascript 进阶之浮点数储存 #15

Ziphwy opened this issue Jan 26, 2021 · 0 comments

Comments

@Ziphwy
Copy link
Owner

Ziphwy commented Jan 26, 2021

0.1 + 0.2 ≠ 0.3 ?

当我们在浏览器输入 0.1 + 0.2,会出现令人匪夷所思的结果:

0.1 + 0.2 = 0.30000000000000004

很多人都知道是浮点数误差导致了,但是其中的原理却说不清楚,下面我们逐步揭开这个 Javascript 中的神秘面纱。

要深入了解这个问题,我们需要提前准备一些知识。

二进制小数

我们日常说的十进制小数 123.45 的含义实际上是:

image

类比十进制,二进制小数 10.01 的也可以用这种方式表达其十进制值:

image

一般地,二进制小数转换十进制小数的公式为:

image

二进制科学记数法

如果我们直接对 10.01 的每一位进行存储,再额外记录小数点位置,似乎就能把一个二进制小数存储下来。

但是,对于 0.00000001 这样的小数,前置的 0 是无意义的,有效数字只有 1,浪费了许多内存。因此,我们可以使用科学记数法标准化后再进行存储。

在十进制中,任意小数可以使用有效数字乘以 10 的幂表示:

image

同样地,我们可以延伸这个方法,在二进制中使用科学记数法:

image

这样的话,我们只需要存储 SME 就可存储一个小数。

回到上面例子:

image

我们只需要存储 01-7 而无需浪费多余的内存。

浮点数的存储方式

IEEE754(二进制浮点数算术标准)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多 CPU 与浮点运算器所采用。

Javascript 遵循 IEEE754 标准,使用双精度浮点数存储数字类型,双精度浮点数占用 64 个字节:

回顾上面的二进制的科学记数法:

image

在计算机存储浮点数时,从高位开始:

  • 第 1 位存放符号 S
  • 第 2 ~ 12 位存放指数 E
  • 第 13 ~ 64 位存放有效数 M

符号位 S

共 1 位,表示浮点数的正负,0 为正数,1 为负数。

指数位 E

共 11 位,存储的是指数的偏移值而不是实际值,也被称为阶码。因为负指数的存在,IEEE754 规定 中间值 01111111111(1023) 作为偏移量,计算规则为:存储值 = 真实值 + 偏移量,

例:

指数  1 存为 10000000000(1024)
指数 -2 存为 01111111101(1021)

所以可以表示的指数值范围实际是 [-1023, 1023] (1024 被使用为特殊值见下面章节) 。

尾数位 M

共 52 位,直接按位存储有效数字而不是具体数值,末位舍入。为了保持不影响原值,不满 52 位时低位需要补 0

观察到二进制科学记数法中,M 的整数部分一定是 1,如:

image

实际上尾数位 M 只存储有效数的小数部分,节省了 1 个字节,可以多表示 1 位精度。

1.01   存储为 0100000000000000000000000000000000000000000000000000
1.1001 存储为 1001000000000000000000000000000000000000000000000000

计算机在读取浮点数时,默认首位为 1,这种浮点数被称为规约形式的浮点数

虽然使用规约形式的浮点数可以表示更高的精度,但是会出现 2 个问题,

  1. 我们无法表示 ±0
  2. 可表示最小数与零的距离,比浮点数之间的距离还要大

为了解决这些问题,IEEE754 规定了在 E00000000000 时,默认首位为 0 而不是 1,用于表示更接近 0 的浮点数,这类浮点数被称为非规约形式的浮点数

实际的例子

利用上面的知识我们可以知道,十进制 2.25,即二进制 10.01 ,科学计数法表示为:

image

符号位 S = 0
指数位 E = 1023 + 1 = 1024 = 10000000000
尾数位 M = [省略高位 1] 001 [ 49  0]

所以可以得出:

0 10000000000 0010000000000000000000000000000000000000000000000000

上面的结果可以使用 FloatConverter 进行验证。

浮点数精度和范围

精度

双精度浮点数的精度完全由 M 位确定,因为位数有限,末位舍入,因此双精度浮点数无法精确表示所有的浮点数。

image

双精度浮点数可以保证 15 位十进制有效数字,在 Javascript 中使用 toPrecision() 方法可以获取存储值。

如十进制的 0.1 表示成二进制为 0.00110011001100(1100 无限循环):

0.00011001100110011001100110011001100110011001100110011001100110011001100...

取有效数字后 52 位,末尾舍入进 1,存储值为:

0 00001111011 1100110011001100110011001100110011001100110011001101

对应的二进制:

0.0001100110011001100110011001100110011001100110011001101

换算回十进制:

0.1000000000000000055511151231257827021181583404541015625

因为末尾舍入进 1,所以存储值比实际值偏大。

最值

根据双精度浮点数的存储方式,可以表示的最大数为:

image

可以表示的最小数为:

image

可以从 Number 中取得:

Number.MAX_VALUE // 最大数 1.7976931348623157e+308
Number.MIN_VALUE // 最小数 5e-324

安全整数

不管是整数还是小数,Javascript 都是使用双精度浮点数存储,浮点数无法精确表示所有小数,当数值足够大时,这个不精确性会扩散到整数。

虽然指数位 E 可以表示 [-1023, 1023] 的指数范围,但尾数位 M 最多表示 53 位有效数字,这意味我们最多精确存储 53 位的整数,一旦超出这个范围,末位都会被舍入,无法精确表示和计算。

// 最大安全整数存储状态
0 00000110101 1111111111111111111111111111111111111111111111111111

// 最小安全整数存储状态
1 00000110101 1111111111111111111111111111111111111111111111111111
  • 当整数在 (2^53, 2^54) 之间,被抹掉了最后 1 位,都变成 0,只能精确表示 2 的倍数。
  • 当整数在 (2^54, 2^55) 之间,被抹掉了最后 2 位,都变成 00,只能精确表示 4 的倍数。
  • ......

安全整数的范围是 [-2^53, 2^53] ,即:

Number.MAX_SAFE_INTEGER // 最大安全整数 9007199254740991 
Number.MIN_SAFE_INTEGER // 最小安全整数 -9007199254740991 

其他特殊值

指数位 E 全是 1 (1024),尾数位 M 全是 0 为无穷:

  • -Infinity
1 11111111111 0000000000000000000000000000000000000000000000000000
  • +Infinity
0 11111111111 0000000000000000000000000000000000000000000000000000

指数位 E 全是 1 (1024),尾数位 M 不全是 0 为非数:

  • NaN
0 11111111111 1111111111111111111111111111111111111111111111111111

0.1 + 0.2 的解释

回到最初的问题,为什么 0.1 + 0.2 ≠ 0.3,因为双精度浮点数无法精确表示 0.10.2,在进行加法时也会丢失一定的精度。

存储时的精度丢失:

0 00001111011 1100110011001100110011001100110011001100110011001101
0 01111111101 1100110011001100110011001100110011001100110011001101

运算时的精度丢失:

0.1 + 0.2 
= 0.001100110011001100110011001100110011001100110011001101 +
  0.0001100110011001100110011001100110011001100110011001101

= 0.0100110011001100110011001100110011001100110011001101

转为十进制:

0.3000000000000000444089209850062616169452667236328125

因为浮点数存储方式是由 IEEE754 标准定义的,所以这并不是 Javascript 特有的,所有遵循 IEEE754 都会有相同的问题。

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant