We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
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
当我们在浏览器输入 0.1 + 0.2,会出现令人匪夷所思的结果:
0.1 + 0.2
0.1 + 0.2 = 0.30000000000000004
很多人都知道是浮点数误差导致了,但是其中的原理却说不清楚,下面我们逐步揭开这个 Javascript 中的神秘面纱。
要深入了解这个问题,我们需要提前准备一些知识。
我们日常说的十进制小数 123.45 的含义实际上是:
123.45
类比十进制,二进制小数 10.01 的也可以用这种方式表达其十进制值:
10.01
一般地,二进制小数转换十进制小数的公式为:
如果我们直接对 10.01 的每一位进行存储,再额外记录小数点位置,似乎就能把一个二进制小数存储下来。
但是,对于 0.00000001 这样的小数,前置的 0 是无意义的,有效数字只有 1,浪费了许多内存。因此,我们可以使用科学记数法标准化后再进行存储。
0.00000001
0
1
在十进制中,任意小数可以使用有效数字乘以 10 的幂表示:
同样地,我们可以延伸这个方法,在二进制中使用科学记数法:
这样的话,我们只需要存储 S,M,E 就可存储一个小数。
S
M
E
回到上面例子:
我们只需要存储 0,1,-7 而无需浪费多余的内存。
-7
IEEE754(二进制浮点数算术标准)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多 CPU 与浮点运算器所采用。
Javascript 遵循 IEEE754 标准,使用双精度浮点数存储数字类型,双精度浮点数占用 64 个字节:
回顾上面的二进制的科学记数法:
在计算机存储浮点数时,从高位开始:
共 1 位,表示浮点数的正负,0 为正数,1 为负数。
共 11 位,存储的是指数的偏移值而不是实际值,也被称为阶码。因为负指数的存在,IEEE754 规定 中间值 01111111111(1023) 作为偏移量,计算规则为:存储值 = 真实值 + 偏移量,
01111111111(1023)
例:
指数 1 存为 10000000000(1024) 指数 -2 存为 01111111101(1021)
所以可以表示的指数值范围实际是 [-1023, 1023] (1024 被使用为特殊值见下面章节) 。
[-1023, 1023]
共 52 位,直接按位存储有效数字而不是具体数值,末位舍入。为了保持不影响原值,不满 52 位时低位需要补 0。
观察到二进制科学记数法中,M 的整数部分一定是 1,如:
实际上尾数位 M 只存储有效数的小数部分,节省了 1 个字节,可以多表示 1 位精度。
1.01 存储为 0100000000000000000000000000000000000000000000000000 1.1001 存储为 1001000000000000000000000000000000000000000000000000
计算机在读取浮点数时,默认首位为 1,这种浮点数被称为规约形式的浮点数。
虽然使用规约形式的浮点数可以表示更高的精度,但是会出现 2 个问题,
为了解决这些问题,IEEE754 规定了在 E 为 00000000000 时,默认首位为 0 而不是 1,用于表示更接近 0 的浮点数,这类浮点数被称为非规约形式的浮点数。
00000000000
利用上面的知识我们可以知道,十进制 2.25,即二进制 10.01 ,科学计数法表示为:
2.25
符号位 S = 0 指数位 E = 1023 + 1 = 1024 = 10000000000 尾数位 M = [省略高位 1] 001 [补 49 个 0]
所以可以得出:
0 10000000000 0010000000000000000000000000000000000000000000000000
上面的结果可以使用 FloatConverter 进行验证。
双精度浮点数的精度完全由 M 位确定,因为位数有限,末位舍入,因此双精度浮点数无法精确表示所有的浮点数。
双精度浮点数可以保证 15 位十进制有效数字,在 Javascript 中使用 toPrecision() 方法可以获取存储值。
toPrecision()
如十进制的 0.1 表示成二进制为 0.00110011001100(1100 无限循环):
0.1
0.00110011001100(1100 无限循环)
0.00011001100110011001100110011001100110011001100110011001100110011001100...
取有效数字后 52 位,末尾舍入进 1,存储值为:
0 00001111011 1100110011001100110011001100110011001100110011001101
对应的二进制:
0.0001100110011001100110011001100110011001100110011001101
换算回十进制:
0.1000000000000000055511151231257827021181583404541015625
因为末尾舍入进 1,所以存储值比实际值偏大。
根据双精度浮点数的存储方式,可以表示的最大数为:
可以表示的最小数为:
可以从 Number 中取得:
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)
(2^54, 2^55)
00
安全整数的范围是 [-2^53, 2^53] ,即:
[-2^53, 2^53]
Number.MAX_SAFE_INTEGER // 最大安全整数 9007199254740991 Number.MIN_SAFE_INTEGER // 最小安全整数 -9007199254740991
指数位 E 全是 1 (1024),尾数位 M 全是 0 为无穷:
1 11111111111 0000000000000000000000000000000000000000000000000000
0 11111111111 0000000000000000000000000000000000000000000000000000
指数位 E 全是 1 (1024),尾数位 M 不全是 0 为非数:
0 11111111111 1111111111111111111111111111111111111111111111111111
回到最初的问题,为什么 0.1 + 0.2 ≠ 0.3,因为双精度浮点数无法精确表示 0.1 和 0.2,在进行加法时也会丢失一定的精度。
0.1 + 0.2 ≠ 0.3
0.2
存储时的精度丢失:
0 00001111011 1100110011001100110011001100110011001100110011001101 0 01111111101 1100110011001100110011001100110011001100110011001101
运算时的精度丢失:
0.1 + 0.2 = 0.001100110011001100110011001100110011001100110011001101 + 0.0001100110011001100110011001100110011001100110011001101 = 0.0100110011001100110011001100110011001100110011001101
转为十进制:
0.3000000000000000444089209850062616169452667236328125
因为浮点数存储方式是由 IEEE754 标准定义的,所以这并不是 Javascript 特有的,所有遵循 IEEE754 都会有相同的问题。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
0.1 + 0.2 ≠ 0.3 ?
当我们在浏览器输入
0.1 + 0.2
,会出现令人匪夷所思的结果:很多人都知道是浮点数误差导致了,但是其中的原理却说不清楚,下面我们逐步揭开这个 Javascript 中的神秘面纱。
要深入了解这个问题,我们需要提前准备一些知识。
二进制小数
我们日常说的十进制小数
123.45
的含义实际上是:类比十进制,二进制小数
10.01
的也可以用这种方式表达其十进制值:一般地,二进制小数转换十进制小数的公式为:
二进制科学记数法
如果我们直接对
10.01
的每一位进行存储,再额外记录小数点位置,似乎就能把一个二进制小数存储下来。但是,对于
0.00000001
这样的小数,前置的0
是无意义的,有效数字只有1
,浪费了许多内存。因此,我们可以使用科学记数法标准化后再进行存储。在十进制中,任意小数可以使用有效数字乘以 10 的幂表示:
同样地,我们可以延伸这个方法,在二进制中使用科学记数法:
这样的话,我们只需要存储
S
,M
,E
就可存储一个小数。回到上面例子:
我们只需要存储
0
,1
,-7
而无需浪费多余的内存。浮点数的存储方式
IEEE754(二进制浮点数算术标准)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多 CPU 与浮点运算器所采用。
Javascript 遵循 IEEE754 标准,使用双精度浮点数存储数字类型,双精度浮点数占用 64 个字节:
回顾上面的二进制的科学记数法:
在计算机存储浮点数时,从高位开始:
S
E
M
符号位 S
共 1 位,表示浮点数的正负,
0
为正数,1
为负数。指数位 E
共 11 位,存储的是指数的偏移值而不是实际值,也被称为阶码。因为负指数的存在,IEEE754 规定 中间值
01111111111(1023)
作为偏移量,计算规则为:存储值 = 真实值 + 偏移量,例:
所以可以表示的指数值范围实际是
[-1023, 1023]
(1024 被使用为特殊值见下面章节) 。尾数位 M
共 52 位,直接按位存储有效数字而不是具体数值,末位舍入。为了保持不影响原值,不满 52 位时低位需要补
0
。观察到二进制科学记数法中,
M
的整数部分一定是1
,如:实际上尾数位
M
只存储有效数的小数部分,节省了 1 个字节,可以多表示 1 位精度。计算机在读取浮点数时,默认首位为
1
,这种浮点数被称为规约形式的浮点数。虽然使用规约形式的浮点数可以表示更高的精度,但是会出现 2 个问题,
为了解决这些问题,IEEE754 规定了在
E
为00000000000
时,默认首位为0
而不是1
,用于表示更接近 0 的浮点数,这类浮点数被称为非规约形式的浮点数。实际的例子
利用上面的知识我们可以知道,十进制
2.25
,即二进制10.01
,科学计数法表示为:所以可以得出:
上面的结果可以使用 FloatConverter 进行验证。
浮点数精度和范围
精度
双精度浮点数的精度完全由
M
位确定,因为位数有限,末位舍入,因此双精度浮点数无法精确表示所有的浮点数。双精度浮点数可以保证 15 位十进制有效数字,在 Javascript 中使用
toPrecision()
方法可以获取存储值。如十进制的
0.1
表示成二进制为0.00110011001100(1100 无限循环)
:取有效数字后 52 位,末尾舍入进 1,存储值为:
对应的二进制:
0.0001100110011001100110011001100110011001100110011001101
换算回十进制:
0.1000000000000000055511151231257827021181583404541015625
因为末尾舍入进 1,所以存储值比实际值偏大。
最值
根据双精度浮点数的存储方式,可以表示的最大数为:
可以表示的最小数为:
可以从
Number
中取得:安全整数
不管是整数还是小数,Javascript 都是使用双精度浮点数存储,浮点数无法精确表示所有小数,当数值足够大时,这个不精确性会扩散到整数。
虽然指数位
E
可以表示[-1023, 1023]
的指数范围,但尾数位M
最多表示 53 位有效数字,这意味我们最多精确存储 53 位的整数,一旦超出这个范围,末位都会被舍入,无法精确表示和计算。(2^53, 2^54)
之间,被抹掉了最后 1 位,都变成0
,只能精确表示 2 的倍数。(2^54, 2^55)
之间,被抹掉了最后 2 位,都变成00
,只能精确表示 4 的倍数。安全整数的范围是
[-2^53, 2^53]
,即:其他特殊值
指数位
E
全是1
(1024),尾数位M
全是 0 为无穷:指数位
E
全是1
(1024),尾数位M
不全是 0 为非数:0.1 + 0.2 的解释
回到最初的问题,为什么
0.1 + 0.2 ≠ 0.3
,因为双精度浮点数无法精确表示0.1
和0.2
,在进行加法时也会丢失一定的精度。存储时的精度丢失:
运算时的精度丢失:
转为十进制:
0.3000000000000000444089209850062616169452667236328125
因为浮点数存储方式是由 IEEE754 标准定义的,所以这并不是 Javascript 特有的,所有遵循 IEEE754 都会有相同的问题。
参考
The text was updated successfully, but these errors were encountered: