IEEE 754 是最广泛使用的 二进制浮点数算术标准。
组成
浮点法表示一个数分为三个部分:符号位 + 指数 + 尾数;通常我们是用二进制的科学计数法表示出来,如 5(101) 记成 \(1.01 * 2^2\)。我们可以称 01 为尾数,2为指数。
IEEE754的表示也分为三个部分:
- 符号位 sign
符号位只占一位,0表示正数,1表示负数 - 阶码 exponent
也就是指数,不过又与指数在数值表示上不同,阶码进行了偏移。为了与实际的指数进行区分,后续我们都称为“阶码”而不是指数。
- 阶码有 e 位,在指数的基础上偏移了 \(2^{e-1}-1\)。
为什么要偏移呢?因为0次方是存在的,但阶码的0要用于表示特殊的数(零或非规格数),因此需要找其他的数代替0。
以单精度的举例,阶码有8位,就要偏移 127,看一个对应图就明白了。
可以看到单精度可表示的指数范围为 -126 ~ 127。
- 尾数 fraction
即表示为科学计数法后的小数部分,如上面的5,尾数部分则是 01。因为二进制中第一个有效数字必定是1,因此可以节约1bit。
IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。其中单精度指数域有 8 个bit,尾数有 23 个bit;双精度指数域为 11 bit,尾数为 52 bit。
意义
符号位 | 阶码 | 尾数 | 意义 |
0/1 | 0 | 0 | ±零 |
0/1 | 0 | 非0 | 非规格化数 |
0/1 | 1 ~ \(2^{e-2}\) | 任意 | 规格化数 |
0/1 | \(2^e - 1\)(全1) | 0 | ±无穷 |
0/1 | \(2^e - 1\)(全1) | 非零 | 非数值 NaN |
1、零
浮点数的0还区分正负,不过一般似乎没有什么区别。只是在除法时有点区别:
double x = 0.0;
double y = -0.0;
1/x == INF; 1/y == -INF;
2、规格化数
规格化的意思是采用科学计数法的规范表示的数。
如单精度的规格化数范围为:
\[±(1.xx···xx × 2^{-126} , 1.xx···xx × 2^{127}) \]
其中x为0或1。
3、非规格化数
非规格化可以用于表示比规格化数还接近0的数。
非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值小1。例如,最小的规格单精度浮点数的阶码为1,指数的实际值为-126;而非规格单精度浮点数的阶码为0,对应的指数实际值也是-126而不是-127。
非规约浮点数源于70年代末IEEE浮点数标准化专业技术委员会酝酿浮点数二进制标准时,Intel公司对渐进式下溢出(gradual underflow)的力荐。当时十分流行的DEC VAX机的浮点数表示采用了突然式下溢出(abrupt underflow)。
如果没有渐进式下溢出,那么0与绝对值最小的浮点数之间的距离将大于相邻的小浮点数之间的距离。例如单精度浮点数的绝对值最小的规约浮点数是1.0*2-126,它与绝对值次小的规约浮点数之间的距离为2-126 * 2-23;如果不采用渐进式下溢出,那么绝对值最小的规约浮点数与0的距离是相邻的小浮点数之间距离的223倍,可以说是非常突然的下溢出到0。这种情况的一种糟糕后果是:两个不等的小浮点数X与Y相减,结果将是0。这对于普通的程序员会很容易陷入迷惑,采用了渐进式下溢出后将不会出现这种情况,也就是采用非规约数表示更接近0的数。采用了非规约数后,0与最近的浮点数(最小的非规约数)的距离也是 2-126 * 2-23。
4、无穷
INF表示无穷,出现无穷的常见情况有:
- 无穷与自身运算, 如负无穷+2依然是负无穷
- 被0除, 例如1/0得到正无穷
- 上溢, 即计算结果超出类型范围
5、非数值
即一些运算过程中出现的非数值情况。NaN还分为两类:
- QNAN,尾数部分最高位为1,一般表示未定义的算术运算结果,最常见的如除0运算;
- SNAN,尾数最高位为0;一般被用于标记未初始化的值,以此来捕获异常。
有趣的现象
连续和不连续
虽然浮点数可以表示很大的数,但是它只在中间部分是连续的,过大的数是不连续的,间隔很大,因为指数很大但尾数的个数是很有限的。而特别小的数因为有非规约数,所以还比较连续。
js最大安全数
JavaScript中的数值统一都是双精度浮点数,js存在一个最大的安全数,为253-1,当我们们表示整数的时候,指数部分为1,只通过尾数表示。因为如果用到高次的阶码,那么大数就不是递增1的了,这在程序中容易出错。
而为什么是253-1呢,因为双精度的尾数有52位,这里省去了第一位有效数字,所以能表示的应该是53位,因此最大的数就是253-1。
前后端开发的时候,后端传给前端的整数值不能过大,否则会出现部分位丢失的问题。我在个人博客项目中,表的分布式id为64位long型,传给前端就会出现截断丢失。