来聊聊IEEE754和0.1+0.2

268 阅读5分钟

首先IEEE754是什么?

IEEE754就是一种表示数值的方法, 属于浮点表示法.

为何会出现浮点表示法?

因为精度不够, 人们需要计算机提供高精度运算, 你可能疑惑, 它精度丢失竟然是为了提高精度? 我们先来看另一种数值表示法: 定点表示法

定点表示法

在存储一个正整数时, 整数可被当作小数点位置固定(始终在数值的最右边, 如下图), 在这种假设中, 小数点是假设的, 不会被存储

对于负数则在上述基础上使用补码进行存储, 不多做介绍了.

我们需要计算机能够存储实数, 不仅要存储整数部分, 还要存储小数部分, 既然要存储小数, 就涉及了精度问题, 即计算机要存储多少位小数? 按照定点表示法, 可以把16位比特位一分为二, 一部分存储整数部分, 另一部分存储小数部分, 这样存储精度就是8位, 缺点也显而易见, 8位的精度远远不够, 而且这样的空间划分直接导致存储范围减小一半, 如果再考虑进负数, 存储范围将再减小一半! 定点表示法存储实数不仅浪费空间而且精度不足.

浮点表示法

因此, 出现了浮点表示法, 浮点表示法允许小数点浮动, 小数点的左右可以有不同数量的数码, 增加了可存储的实数范围.

浮点表示法由3部分组成: 符号、位移量、定点数

理解浮点表示法, 可以先类比科学计数法:

在科学计数法(eg: 1.0341 × 1021)中, 定点部分在小数点左边只有一位(1),位移量是10的幂次(21), 定点数就是1.0341, 符号位+.

使用同样的方法, 我们也可以表示二进制数:

即: + 1.01001 × 232

科学计数法(用于十进制)和浮点表示法(用于二进制)都在小数点左边使用了唯一的非零数码, 这称为规范化.

在十进制系统中, 非零数码可能是1到9.

但在二进制系统中, 非零数码只能是1, 所以这个非零数码就不需要存储了(小数点紧跟非零数码, 小数点也不需要被存储)

有了二进制数规范化后, 计算机只需要存储该数的三部分信息(例如二进制数+ 1000111.0101):

注意: 尾数是小数点后的部分.

至此, 你应该明白浮点表示法了.

余码系统

接着我们再介绍余码系统:

在浮点表示法中, 位移量(即指数)部分, 它是一个有符号的, 表示多少位小数点应该左移或右移的幂次(例如1.0101×215 1.0110×2-15). 为了表示正整数或负整数, 将正整数(一个偏移量)添加到每个数字中, 将这些数统一移到非负的一边, 这便是余码系统, 在余码系统中, 正整数和负整数都可以作为无符号数存储.

其中的偏移量值是2e-1-1, e是内存单元存储指数的大小.

我们以一个4位存储单元表示16个整数为例:

偏移量 = 24-1-1 = 7

这16个整数有正有负, 为每个数加上偏移量可以把他们都移到正数部分, 即可使用无符号数进行存储:

此时的新系统称为余7码(或 偏移量为7的偏移表示法)

IEEE标准

我们介绍浮点表示法的IEEE标准.

这里可分为单精度(指数域为8个比特)和双精度, 单精度使用了余127码, 双精度使用了余1023码.

余码表示法仅仅用于计算浮点表示法的位移量部分, 以下介绍几个计算示例:

  • 求十进制数5.75的余127码表示法

a. 符号为正, 所以S = 0

b. 十进制转为二进制: 5.75 = (101.11)2

c. 规范化: (101.11)2=(1.0111)2×22

d. E = 2+127 = 129 = (10000001)2,

e: M = 0111, 需要在M的右边增加19个0使之称为23位

存储在计算机中的数字是:

  • 求十进制数-0.0234375的余127码表示法

a. 符号为负, S=1

b. 十进制转为二进制: 0.0234375=(0.0000011)2

c. 规范化: (0.0000011)2=(1.1)2×2-6

d. E = -6 + 127 = 121 = (01111001)2

e: M = (1)2

存储在计算机中的数字是:

  • 位模式(11001010000000000111000100001111)2以余127码格式存储与内存中, 求该数字的十进制计数法值

a. 观察可得

b. 符号为负

c. 位移量 = E - 127 = 148 - 127 = 21

d.去规范化: (1.00000000111000100001111)2 × 221= (1000000001110001000011.11)2

e. 二进制转十进制: (1000000001110001000011.11)2=2104378.75

f. 考虑符号, 最终该数字是-2104378.75

对于特殊的实数0.0, 约定在这种情况下, 符号、指数、尾数都设为0.

Javascript场景下

JS的Number数据类型使用IEEE754格式表示整数和浮点值.

由于0.1和0.2和0.3不能准确地表示为IEEE754浮点格式, 因而会存在精度丢失问题:

详细计算0.1+0.2见: zhuanlan.zhihu.com/p/357676697

0.1.toFixed(20) // 0.10000000000000000555
0.2.toFixed(20) // 0.20000000000000001110
0.3.toFixed(20) // 0.29999999999999998890
0.1+0.2 // 0.30000000000000004

利用乘法可实现准确计算吗?

// 因为1, 2, 3都可以准确地表示为IEEE754格式
(0.1 * 10).toFixed(20) // 1.00000000000000000000
(0.2 * 10).toFixed(20) // 2.00000000000000000000
(0.3 * 10).toFixed(20) // 3.00000000000000000000
(0.1 * 10 + 0.2 * 10).toFixed(20) // 3.00000000000000000000

// 因为0.3, 0.2都无法准确表示为IEEE754格式, 因而会有精度问题
// 网传利用先乘再除可以解决精度问题, 其实并不能
((0.1 * 100 + 0.2 * 100) / 100).toFixed(20) // 0.29999999999999998890
(0.2 + 0.2 + 0.2 + 0.2 + 0.2 + 0.2 + 0.2 + 0.2 + 0.2 + 0.2).toFixed(20) // 1.99999999999999977796