小甲鱼 发表于 2024-5-31 18:10:27

一次性讲清楚IEEE 754浮点数标准

一次性讲清楚IEEE 754浮点数标准

缘起

下面是一个典型的 JavaScript 精度误差:

> 0.1 + 0.2
0.30000000000000004
类似 0.1 + 0.2 结果不完全等于 0.3 的编程语言不仅仅限于 JavaScript。

实际上,大多数现代编程语言在处理浮点数时都会遇到类似的问题,C、C++、JAVA、Python、Go…… 都如此,谁也别笑谁~

究其原因,是因为这些语言通常都使用 IEEE 754 标准来表示浮点数,而这个标准在表示某些十进制小数时存在精度限制问题。


借此机会,小甲鱼顺便给大家一次性讲清楚 IEEE 754 浮点数标准的前世今生,及具体的实现原理。

注意:本篇文章非必学的内容,不掌握也不会影响你成为 JavaScript 大拿!


IEEE 754 的前世今生

在 IEEE 754 标准出现之前,不同的计算机系统使用各自不同的方法来处理浮点数。

不难想象,这种 “你用你的标准,我用我的标准” 的情况必将导致程序的浮点数处理混乱和不一致。

为了解决这个问题,国际电气与电子工程师协会(Institute of Electrical and Electronics Engineers,IEEE)启动了一个标准化项目,旨在制定一个通用的浮点数运算标准。

于是,IEEE 754 标准在 1985 年被正式采纳,这个标准不仅规定了浮点数的格式,还涵盖了运算规则、异常处理、舍入方式等多个方面。

不要小看这个标准,该标准的主要贡献者威廉·卡汉(William Kahan)因此直接获得了图灵奖(计算机界的诺贝尔奖)。


浮点数的表示

在 IEEE 754 标准中,浮点数可以通过下面公式来描述:

$V = (-1)^{S} \times M \times 2^{E}$

该公式包括三个主要部分:符号位(S)、指数位(E,也成为阶码位)和尾数位(M,也称为小数位或效数字位)。

符号位(Sign bit)

符号位决定了该浮点数的正负值,0 代表正数,1 代表负数:


[*]$(-1)^{0} = 1$
[*]$(-1)^{1} = -1$

指数位(Exponent)

指数位用于表示浮点数的指数部分,但它存储的不是直接的指数值,而是一个偏移量(或称为偏置)表示的值。


[*]在单精度(32 位浮点数)中,指数是 8 位,偏移量为 127。
[*]在双精度(64 位浮点数)中,指数是 11 位,偏移量为 1023。

真实的指数值为存储的值减去偏移量的结果(请参考下面 “规格化数” 中的计算演示)。

尾数位(Mantissa)

尾数部分存储浮点数的实际数字(或称为有效数字),不包括数字序列中最左边的 1(对于规格化数)。

这意味着浮点数的精度可以得到增加,因为存储空间用于表示更多的有效数字。

在单精度中,尾数位占 23 位,在双精度中占 52 位。


由于 JavaScript 采用的是双精度浮点数格式(64 位),所以在内存中浮点数的封装如下:



燃鹅,这里并不是直接用二进制表示各个字段的值,不然小甲鱼就不用写这篇文章了……

下面内容比较复杂,请做好心理准备,再次强调:非必学的内容,看不懂也没关系。

{:10_248:}


浮点数的分类

根据指数位和尾数位的不同组合,IEEE 754 定义了 4 种类型的值:规格化数、非规格化数、无穷大和 NaN。


规格化数(Normalized Numbers)

绝大多数浮点数在 IEEE 754 标准中确实是以规格化数的形式存储的。

当指数位不全为 0 或不全为 1 时,浮点数被称为规格化数。

这种数有一个隐含的最高位 1(即 1.XXXXX 的形式)。

对于规格化的数,浮点数的表示遵循以下形式:

$V = (-1)^{S} \times 1.XXXXX \times 2^{E}$

这里使用 1.XXXXX 尾数部分所表示的小数(在二进制下),例如尾数位为 101100...0(总共 52 位),则 M = 1.101100...0(二进制)

假设我们有一个双精度浮点数,其位模式如下:

0100000001101000000000000000000000000000000000000000000000000000

让我们来分解这 64 位的值:


[*]符号位(S)-> 0 -> 正数
[*]指数位(E)-> 10000000110 -> 二进制转换为十进制得到 1030 -> 实际指数是 1030 - 1023 = 7
[*]尾数位(M)-> 1000000000000000000000000000000000000000000000000000 -> 二进制小数为 1.1000000000000000000000000000000000000000000000000000(这里有一个隐含的最高位 1)-> 1.1 转换为十进制数 -> $2^{0} + 2^{-1} = 1.5$

提示 1:指数位它存储的不是直接的指数值,而是一个偏移量(或称为偏置)表示的值,64 位浮点数的偏移量为 1023。

提示 2:关于进制转换可以看下这篇 -> 深入浅出进制转换。

因此,这个数值是:

$V = (-1)^{0} \times 1.5 \times 2^{7} = 1 \times 1.5 \times 128 = 192.0$


非规格化数(Denormalized Numbers)

在 IEEE 754 标准中引入非规格化数的主要目的是为了填补 0 和最小的规格化数之间的数值空白。

非规格化数能够显著扩展浮点数系统可以表示的数值范围,尤其是在数值非常非常小的场景下。

非规格化数的特点是指数位全为 0(即二进制表示为 00000000000),而尾数位不全为 0。


假设我们有一个双精度浮点数,其位模式如下:

0000000000000000000000000000000000000000000000000000000000000001

让我们来分解这 64 位的值:


[*]符号位(S)-> 0 -> 正数
[*]指数位(E)-> 00000000000 -> 非规格化数的标记
[*]尾数位(M)-> 0000000000000000000000000000000000000000000000000001

对于非规格化数来讲,指数计算是:$E = 1 - 偏移量$,

64 位偏移量是 1023,所以指数部分为 $2^{1 - 1023} = 2^{1022}$。

尾数位只有最低位是 1,所以尾数值是 $2^{-52}$。

因此,最终结果是:

$V = (-1)^{0} \times 2^{-52} \times 2^{-1022} = 2^{-1074}$

这个值是极其接近于零的正数,展示了非规格化数能够表示非常小的数值,从而增加了浮点数系统的动态范围。


无穷大(Infinity)

无穷大在双精度浮点数中的表示如下:


[*]符号位(S)-> 0 表示正无穷大(Infinity),1 表示负无穷大(-Infinity)
[*]指数位(E)-> 11111111111(全部为 1)
[*]尾数位(M)-> 0000000000000000000000000000000000000000000000000000(全部为 0)

正无穷大:

0111111111110000000000000000000000000000000000000000000000000000

负无穷大:

1111111111110000000000000000000000000000000000000000000000000000


非数值(NaN)

NaN 用于表示未定义的或无法表示的数值结果。

它有两种形式:静默 NaN(quiet NaN)和信号 NaN(signaling NaN)。


[*]符号位(S)-> 可以是 0 或 1,但通常对 NaN 的解释不依赖于符号位
[*]指数位(E)-> 11111111111(全部为 1)
[*]尾数位(M)-> 不全为 0(至少有一个位是 1)

静默 NaN 通常在尾数的最高位设为 1(确保它是静默的):

0111111111111000000000000000000000000000000000000000000000000000

信号 NaN 的尾数位通常不同,具体取决于系统的实现:

0111111111110100000000000000000000000000000000000000000000000000


实战

1. 如何使用 IEEE 754 保存 3 这个整数?

对于正整数 3,符号位的值是 0。

将 3 转换为二进制是 $3_{(10)} = 11_{(2)}$,由于规格化数有一个隐含的 1(即 1.XXXXX),因此:

$3_{(10)} = 11_{(2)} = 1.1 \times 2^{1}$

从而得到尾数位的值是 1000000000000000000000000000000000000000000000000000

指数是 1,但指数位的计算需要考虑偏移量,64 位的偏移量是 1023,所以指数位存放的值则应该是:

$1 + 1023 = 1024_{(10)} = 10000000000_{(2)}$

最终的二进制表示是:

0100000000001000000000000000000000000000000000000000000000000000


2. 如何使用 IEEE 754 保存 3.14 这个小数?

对于正数 3.14,符号位的值是 0。

将 3.14 转换为二进制表示涉及到整数部分和小数部分的转换~

整数部分 3 转换为二进制是:

$3_{(10)} = 11_{(2)}$

小数部分 0.14 转换为二进制:


[*]0.14 * 2 = 0.28 取整数部分 0
[*]0.28 * 2 = 0.56 取整数部分 0
[*]0.56 * 2 = 1.12 取整数部分 1
[*]0.12 * 2 = 0.24 取整数部分 0
[*]0.24 * 2 = 0.48 取整数部分 0
[*]0.48 * 2 = 0.96 取整数部分 0
[*]0.96 * 2 = 1.92 取整数部分 1
[*]0.92 * 2 = 1.84 取整数部分 1
[*]...

如果继续算下去,会发现 0.14 的二进制表示实际上是一个无限循环的小数(小甲鱼:得算到第 41 次,这里就列长龙了……)

将整数和小数部分的二进制结果合并后得到:

$3.14_{(10)} \approx 11.0010001111010111000010..._{(2)} = 1.10010001111010111000010... \times 2^{1}$

指数是 1,但指数位的计算需要考虑偏移量,64 位的偏移量是 1023,所以指数位存放的值则应该是:

$1 + 1023 = 1024_{10} = 10000000000_{2}$

由于是无线循环,尾数位需要截断到 52 位:

1001000111101011100001010001111010111000010100011110101110000100

最终的二进制表示是:

0100000000001001000111101011100001010001111010111000010100011110101110000100


灵魂拷问

1. 为什么 0.1 + 0.2 不等于 0.3 呢?

**** Hidden Message *****

2. Number.MAX_VALUE 和 Number.MIN_VALUE 的值是为什么是 1.7976931348623157e+308 和 5e-324?

**** Hidden Message *****

3. Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 的值是为什么是 9007199254740991 和 -9007199254740991?

**** Hidden Message *****

某一个“天” 发表于 2024-5-31 18:30:29

{:10_264:}

clollipops 发表于 2024-6-3 19:27:54

似懂非懂

zhae89 发表于 2024-6-3 19:28:09

又学到新知识了

ABitGinger 发表于 2024-6-3 19:29:07

深奥的知识又增加了{:10_329:}

happycarl 发表于 2024-6-3 19:29:40

学习学习

lu10086 发表于 2024-6-3 19:30:21

又学到了

疯狗马德森 发表于 2024-6-3 19:32:21

本来会学完了感觉不会了

忘川hd 发表于 2024-6-3 19:33:28

好好学习

神荼Q 发表于 2024-6-3 19:34:14

IEEE国际电气与电子工程师协会。前几天刚知道这个协会,全英文的网站看着是真的懵。虽然这篇文章看着也有点懵吧,但是至少能看懂那么一点点

想个好名字@ 发表于 2024-6-3 19:34:38

新知识有点太深奥了

天空之算法 发表于 2024-6-3 19:40:30

比较深奥

13351890899 发表于 2024-6-3 19:43:26

打卡成功了

向尚的小六 发表于 2024-6-3 19:46:26

又学到了

赵晨申 发表于 2024-6-3 19:46:30

学习新知识,不断进步,加油!

画风华 发表于 2024-6-3 19:46:53

加油!继续学习!前进!

yasi 发表于 2024-6-3 19:57:53

很深奥,需要好好钻研一下

格子penbeat 发表于 2024-6-3 20:04:16

又涨新知识了

spt1314 发表于 2024-6-3 20:11:47

虽然还不是很懂但是感觉涨姿势了

泼墨染笛香 发表于 2024-6-3 20:24:43

所以金融类数据存储要注意
页: [1] 2 3
查看完整版本: 一次性讲清楚IEEE 754浮点数标准