MENU

IEEE 754 基本浮点数的精度和范围

2020 年 04 月 25 日 • 阅读: 1963 • 后端

IEEE,Institute of Electrical and Electronics Engineers,电气电子工程师学会(美)的缩写。754 标准,是其制定的,关于计算机软硬件中浮点数(小数)的表示和运算的标准,已被各大硬件厂商和编程语言采用,相信学过计算机组成原理、微机系统接口与原理,或任意一门编程语言的同学,对它都既熟悉又陌生,熟悉的是它的名字,陌生的是它的细节,本文将让你和它更熟悉一点。

表示格式

标准规定,浮点数以二进制科学计数法形式存储,具体分 符号 sign指数 exponent分数 fraction 三部分。实际值可由如下公式计算:

value = sign x exponent x fraction

其中,分数为二进制小数,小数点前隐含一个 0 或 1。如果该小数绝对值大于 0 且小于 1,则小数点前为 0,否则为 1。前者称为 非规约形式的浮点数,后者为 规约形式的浮点数

指数采用 移码 存储,使得 整个浮点数能按字典序比较大小。移码等于实际指数加 偏移量。记指数位数为 e,对于 规约形式的浮点数,其偏移量等于 2^(e-1) - 1,即 [0, 2^e-1] 的 中间值非规约形式的浮点数 的偏移量比规约形式 小 1

举例来说,32 位浮点数,指数占 8 位,规约形式浮点数的偏移量为 [0, 255] 的中间值 127,非规约形式为 126。64 位浮点数,指数占 11 位,规约形式浮点数的偏移量为 [0, 2047] 的中间值 1023,非规约形式为 1022。

浮点数指数部分的实际取值范围是 [-2^(e-1)+2, 2^(e-1)-1],其中 e 为指数所占位数。e 位二进制共能表示 2^e 个数,前述区间少了 2 个,因为左边的 -2^(e-1)+1 被保留用作表示 0,右边的 2^(e-1) 被保留用作表示 无穷大和 NaN

举例来说,32 位浮点数,指数占 8 位,实际取值范围是 [-126, 127],-127 用作表示 0,128 用作表示无穷大和 NaN。64 位浮点数,指数占 11 位,实际取值范围是 [-1022, 1023],-1023 用作表示 0,1024 用作表示无穷大和 NaN。

特殊值

下文中指数均为移码,小数为二进制形式。

  1. 零。若 指数0小数部分 也为 0,则该数为 ±0
  2. 无穷。若 指数2^e-1小数部分0,则该数为 ±∞
  3. 非数。若 指数2^e-1小数部分0,则该数为 NaN
形式指数小数
00
非规约形式0大于 0 小于 1
规约形式1 到 2^e-2大于等于 1 小于 2
无穷2^e-10
NaN2^e-1非 0

可以看到除了 NaN,从上至下,数值的 绝对值 和对应 指数 都是越来越大的。下面以 32 位单精度浮点数 符号位( 1 位)+ 指数域( 8 位)+ 尾数域( 23 位) 为例,直观介绍其在计算机中的存储。下表中,最大最小均指 绝对值,符号位的 * 表示 可正可负,正为 0,负为 1。

类别符号位实际指数偏移指数指数域尾数域数值
1*0127低 7 位为 1全 0±1.0
*-127(保留)0全 0全 0±0.0
最小非规约数*-1260(偏 126)全 0最低位为 1±2^(-126)*2^(-23)
最大非规约数*-1260(偏 126)全 0全 1±2^(-126)*(1-2^(-23))
最小规约数*-1261最低位为 1全 0±2^(-126)
最大规约数*127254高 7 位为 1全 1±2^127*(2-2^(-23))
无穷*128(保留)255全 1全 0±∞
NaN*128(保留)255全 1非全 0非数

着重关注以下 4 点:

  1. 实际指数的 -127 和 128 保留,分别表示 0 和 ∞/NaN。
  2. 规约数指数偏移量为 127,非规约减 1。
  3. 最大非规约数尾数域 + 最小非规约数尾数域 = 1
  4. 最大规约数尾数域 + 最小非规约数尾数域 = 2

关于 3,4 两点,作二进制加法即可理解:

第 3 点
  0. 111 1111 1111 1111 1111 1111 1111 1111 (最大非规约数尾数域)
+ 0. 000 0000 0000 0000 0000 0000 0000 0001 (最小非规约数尾数域)
-------------------------------------------
= 1. 000 0000 0000 0000 0000 0000 0000 0000 (小数点前为隐含值)

第 4 点
  1. 111 1111 1111 1111 1111 1111 1111 1111 (最大规约数尾数域)
+ 0. 000 0000 0000 0000 0000 0000 0000 0001 (最小非规约数尾数域)
-------------------------------------------
= 2. 000 0000 0000 0000 0000 0000 0000 0000 (小数点前为隐含值)

64 位双精度浮点数 符号位( 1 位)+ 指数域( 11 位)+ 尾数域( 52 位) 相应值如下:

类别符号位实际指数偏移指数指数域尾数域数值
1*01023低 10 位为 1全 0±1.0
*-1023(保留)0全 0全 0±0.0
最小非规约数*-10220(偏 1022)全 0最低位为 1±2^(-1022)*2^(-52)
最大非规约数*-10220(偏 1022)全 0全 1±2^(-1022)*(1-2^(-52))
最小规约数*-10221最低位为 1全 0±2^(-1022)
最大规约数*10232046高 10 位为 1全 1±2^1023*(2-2^(-52))
无穷*1024(保留)2047全 1全 0±∞
NaN*1024(保留)2047全 1非全 0非数

同理有以下 4 点:

  1. 实际指数的 -1023 和 1024 保留,分别表示 0 和 ∞/NaN。
  2. 规约数指数偏移量为 1023,非规约减 1022。
  3. 最大非规约数尾数域 + 最小非规约数尾数域 = 1
  4. 最大规约数尾数域 + 最小非规约数尾数域 = 2

比较大小

正数一定大于负数,无穷大于所有数,非数不能比较大小;除此之外,则可按指数域和尾数域的字典顺序比较。

舍入规则

很多十进制小数 转为 二进制会 超出尾数域位数 甚至 无限循环,上述情况在 运算过程中 也有可能发生,这时就需要丢弃部分位,以满足浮点数存储要求,这些原因都导致浮点数不能精确表示所有实数。IEEE 754 规定了以下 4 种舍入规则:

  • 舍入到最接近:将结果舍入到最接近且可以表示的值,若存在两个一样接近的值,取偶数(二进制以 0 结尾)。
  • 向正无穷方向舍入。
  • 向负无穷方向舍入。
  • 向 0 舍入。

其中第一种是默认规则,可通过配置指定其他几种。下面以十进制为例,说明以上舍入方法。

round(0.5)=0 // 舍入到最近,0.5 到 0 和 1 距离相等,取偶数 0
ceil(1.324)=2 // 向正无穷舍入
floor(1.324)=1 // 向负无穷舍入
(int)1.324=1 // 向 0 舍入

精度损失

由于浮点数 位数有限,不可能精确表示所有实数,浮点运算必然存在精度损失。

十进制转二进制

十进制小数转二进制分两部分,整数和小数。整数采用 除二取余,小数采用 乘二取整法。举例来说,十进制 7.1 转为 32 位单精度二进制浮点数 的过程如下:先将整数 7 转为二进制 111,再将小数 0.1 转为二进制,以下是 乘二取整 过程。

0.1 x 2 = 0.2 取 0
0.2 x 2 = 0.4 取 0
0.4 x 2 = 0.8 取 0
0.8 x 2 = 1.6 取 1
0.6 x 2 = 1.2 取 1,下面将是 0011 无限循环
0.2 x 2 = 0.4 取 0
0.4 x 2 = 0.8 取 0
0.8 x 2 = 1.6 取 1
0.6 x 2 = 1.2 取 1
...

可见 0.1 这个十进值小数转为二进制是无限位的,而单精度总共就 32 位,所以只能取其近似值。32 位浮点数尾数是 23 位,因此先采用 就近舍入 取 23 位 000 1100 1100 1100 1100 1101,现在整个浮点数是 111.000 1100 1100 1100 1100 1101。然而还要保证小数点前只为 0/1,所以要把小数点左移 2 位,这样尾数右边就要再丢掉 2 位,采用 就近舍入 最终变为 2^2 * 1.110 0011 0011 0011 0011,按照标准格式表示,符号位为 0,指数域存储移码 (2+127) 的二进制 1000 0001,尾数域 110 0011 0011 0011 0011,即

0 1000 0001 110 0011 0011 0011
s |   e   | |       f        |

从上面乘二取整的过程可以看出,0.2, 0.4, 0.6, 0.8 的二进制都是无限位。又因为

0.3 x 2 = 0.6
0.7 x 2 = 1.4
0.9 x 2 = 1.8

所以 0.3, 0.7, 0.9 的二进制也是无限循环的,总结下来,0.1, 0.2, ... , 0.9 这 9 个小数的二进制,只有 0.5 不是无限循环,其他 8 个都无法精确表示。

二进制转十进制

观察以下二进制小数转十进制:

0.1        = 2^-1 => 0.5
0.01       = 2^-2 => 0.25
0.001      = 2^-3 => 0.125
0.0001     = 2^-4 => 0.0625
0.00001    = 2^-5 => 0.03125
0.000001   = 2^-6 => 0.015625
0.0000001  = 2^-7 => 0.0078125
0.00000001 = 2^-8 => 0.00390625
...

由于结果都是 0.x5 形式,而从上至下每次都要除以一个 2 将其变为 0.x25,即末尾会增加一位。又因为前缀 0.x 除以 2 的位数只增不减(任意小数除以 2 位数不会减少),所以从上至下,结果位数总是增加的。

因此上述各个等式任意相加 除了第一个等式,允许其他等式自身相加,都无法消掉结尾的 5,结果总是 0.x5 形式,即有结论 0.x1 = 2^k = 0.y5,其中 x 可以是任意多个 0/1y 可以是任意多个 0-9

事实上,上述等式的任意相加,等价于各个二进制位取 0/1,其所有结果组成的集合就是所有二进制浮点数。反过来说,一个十进制小数如果不以 5 结尾,则不能精确转为二进制浮点数

我们知道,m 位十进制数有 10^m 个(每一位有 10 种可能性)。同样地,m 位二进制数有 2^m 个。所以,m 位十进制数能精确用 m 位二进制浮点数表示的比例为 (2/10)^m,即 0.2^m。对于 32 和 64 位浮点数,其有效二进制位分别为 24 和 53 位,该比例分别为 1.6777216 x 10^(-17)0.9007199247400992 x 10^(-37)。可以看到 十进制整数越大越不可能转为精确的二进制浮点数

运算过程

二进制浮点数的加减运算分 对阶、尾数求和、规格化、舍入和溢出判断 5 步。下面以常考面试题举例,说明上述过程。

问题一:为何以下 JavaScript 代码输出 false?

console.log(0.1 + 0.2 == 0.3);

解析:JS 中的数字都是 IEEE 754 64 位双精度浮点数,进行十进制运算大体分三步:

  1. 十进制转二进制浮点数
  2. 二进制数运算
  3. 二进制结果转十进制

首先完成 进制转换

尾数
0.1 => 0.0 0011 0011 0011 0011 0011 0011 0011 0011 .... 循环
0.2 => 0.0 0110 0110 0110 0110 0110 0110 0110 0110 .... 循环

规格化
0.1 => 2^(-4) x 1.1001 1001 1001 1001 1001 1001 1001 .... 循环
0.2 => 2^(-3) x 1.1001 1001 1001 1001 1001 1001 1001 .... 循环

转 64 位浮点数标准格式,尾数就近舍入到 52 位,此处发生精度损失
0.1 => 0,011 1111 1011,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001(1)
0.1 => 0,011 1111 1011,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010(取最近偶数)
0.2 => 0,011 1111 1100,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

接下来进行二进制运算,由于阶数不同,先 对阶小阶向大阶看齐,小阶尾数右移 阶差 位。之所以不左移是因为左边为高位,丢掉损失很大。

0.1 尾数右移 1 位,左边移入隐含的 1,右边移出一个 0,再次产生误差
0.1 => 0,011 1111 1100,1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101(0)
0.1 => 0,011 1111 1100,1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101(取最近)
0.2 => 0,011 1111 1100,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

接下来进行 尾数求和

  0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101
+ 1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
--------------------------------------------------------------------
 10.0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111

将尾数 规格化,指数加 1,尾数右移 1 位,就近 舍入,再次产生误差。

1.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011(1) x 2
1.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100 x 2 (取最近偶数)

转为标准格式。

0.1 + 0.2 = 0,011 1111 1101,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100
      0.3 = 0,011 1111 1101,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011

可以看出两者相差 2^(-52-2),并非完全相等。

问题二:写出以下 JavaScript 代码的运行结果

var one = 0.1, two = 0.2, six = 0.6, eight = 0.8;
[two-one == one, eight-six == two];

答案:[true, false]

解析:

前半部分转 IEEE 754
0.1 => 0,011 1111 1011,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
0.2 => 0,011 1111 1100,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

对阶
0.1 => 0,011 1111 1100,1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101
0.2 => 0,011 1111 1100,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

尾数求和
0.2 =>   1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
0.1 => - 0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101
---------------------------------------------------------------------------
0.1 =>   0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101
结果和 0.1 完成相同,因此是 true

后半部分转  IEEE 754
0.6 x 2 = 1.2
0.2 x 2 = 0.4
0.4 x 2 = 0.8
0.8 x 2 = 1.6
0.6 x 2 = 1.2
0.2 x 2 = 0.4
0.4 x 2 = 0.8
...

尾数二进制
0.6 => 0.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001(1)
0.8 => 0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100(1)

尾数取 52 位,就近舍入
0.6 => 0.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
0.8 => 0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101

无需对阶,尾数求和
0.8 =>   0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101
0.6 => - 0.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
---------------------------------------------------------------------------
         0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011
    =>   1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1000
0.2 =>   1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
结果和 0.2 最后两位不同,因此是 false

问题三:解释以下 JavaScript 中 Number 对象的几个静态属性

Number.EPSILON => 1.0 与距离其最近的可表示的浮点数的差值 => 2^(-52),IEEE 754 格式如下
0,000 0000 0000,0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001

Number.MAX_VALUE => 可以表示的最大有限数 => 2^1023 x [2-2^(-52)],IEEE 754 格式如下
0,111 1111 1110,1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111

Number.MIN_VALUE => 可以表示的最接近 0 的正数 => 2^(-1022)*2^(-52),IEEE 754 格式如下
0,000 0000 0000,0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001

Number.MAX_SAFE_INTEGER => 可以精确表示和比较的最大整数 => 2^52 x [2-2^(-52)],IEEE 754 格式如下
0,100 0011 0011,1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111,实际值如下
  2^52 x 1.1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
+ 2^52 x 0.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001
-----------------------------------------------------------------
  2^53 x 1.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
+ 2^53 x 0.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001
-----------------------------------------------------------------
  2^53 x 1.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 => 2^53 x 1 + 2^53 x 2^(-52) => 2^53 + 2
也就是说,虽然最低位只加 1,也就是 2^(-52),但系数是 2^53,乘起来就是 2,从十进制角度就是每次至少加 2

Number.MIN_SAFE_INTEGER => 可以精确表示和比较的最小整数 => -2^52 x [2-2^(-52)],IEEE 754 格式如下
1,100 0011 0011,1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111,实际值如下
-2^52 x 1.1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
-2^52 x 0.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001
---------------------------------------------------------------
-2^53 x 1.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
-2^53 x 0.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001
---------------------------------------------------------------
-2^53 x 1.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 => -2^53 x 1 - 2^53 x 2^(-52) => -2^53 - 2
也就是说,虽然最低位只减 1,也就是 2^(-52),但系数是 -2^53,乘起来就是 -2,从十进制角度就是每次至少减 2

问题四:写出以下 Java 代码的运行结果

public static void main(String[] args) throws Exception {
    System.out.println(Math.min(Integer.MIN_VALUE, 0));
    System.out.println(Math.min(Float.MIN_VALUE, 0.0f));

    try {
        int i = 1 / 0;
        System.out.println(i);
    } catch (Exception e) {
        System.out.println("exception 1 / 0");
    }

    try {
        float f1 = 1.0f / 0.0f;
        System.out.println(f1);
        System.out.println(f1 == f1);
    } catch (Exception e) {
        System.out.println("exception 1.0f / 0.0f");
    }

    try {
        float f2 = 0.0f / 0.0f;
        System.out.println(f2);
        System.out.println(f2 == f2);
    } catch (Exception e) {
        System.out.println("exception 0.0f / 0.0f");
    }
}

答案

结果
-2147483648
-0.0
exception 1 / 0
Infinity
true
NaN
false

原因
1. Integer 是 int 的包装类,底层仍为 int。int 占 4 字节 32 位,因此取值范围 [-2^31, 2^31-1],2^31=2147483648
2. 整数不能除以 0
3. IEEE 754 有限值除以 0 结果为无穷大
4. IEEE 754 规定无穷等于自身
5. IEEE 754 规定一个数无法确定时,它是 NaN
6. IEEE 754 规定 NaN 与任何数比较都是 false(除了 !=)

问题五:写出以下 C++ 代码的运行结果

float f_v1 = 20;
float f_v2 = 20.3;
float f_v3 = 20.5;

double d_v1 = 20;
double d_v2 = 20.3;
double d_v3 = 20.5;

cout << ((f_v1 == d_v1)?"true":"false") << endl;
cout << ((f_v2 == d_v2)?"true":"false") << endl;
cout << ((f_v3 == d_v3)?"true":"false") << endl;

答案

结果
true
false
true

原因
20 能精确转为二进制,且不超出 float 范围 => 1 0100。上文说过,0.5 能精确转为二进制,且不超出 float 范围 => 0.1,剩下的 0.1-0.4, 0.6-0.9 都不行
0.3 x 2 = 0.6
0.6 x 2 = 1.2
0.2 x 2 = 0.4
0.4 x 2 = 0.8
0.8 x 2 = 1.6
0.6 x 2 = 1.2
...

f_v1 => 20.0 => 1 0100.0                          => 1.010 0000 0000 0000 0000 0000
f_v2 => 20.3 => 1 0100.0100 1100 1100 1100 110(0) => 1.010 0010 0010 0010 0010 0010
f_v3 => 20.5 => 1 0100.1                          => 1.010 0100 0000 0000 0000 0000

d_v1 => 20.0 => 1 0100.0                                                              => 1.010 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0
d_v2 => 20.3 => 1 0100.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100(1) => 1.010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 1
d_v3 => 20.5 => 1 0100.1                                                              => 1.010 0100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0

最终比较,要把 float 转 double,所以
f_v1 => double,直接补 0,和 d_v1 相等
f_v2 => double,直接补 0,和 d_v2 不等
f_v3 => double,直接补 0,和 d_v3 相等

总结

由于各大编程语言都用 IEEE 754 标准实现浮点数,所以它们的结果大多数情况下都是不准确的,不能按照十进制运算理解,特别是比较大小时。产生误差的具体细节为:

  1. 十进制转二进制可能超出尾数位数,甚至无限循环,此时必然产生舍入误差。
  2. 对阶时,小阶尾数右移可能产生舍入误差。
  3. 规格化时,尾数如需右移同样可能产生舍入误差。
  4. 单精度表示整数时,最大精度为 2^24 - 1。
  5. 双精度表示整数时,最大精度为 2^53 - 1。

高精度计算需借助相应语言采用其他方法实现的类库,如 Java 的 BigDecimal,JavaScript 的 mathjs 等。

参考资料

TG 大佬群 QQ 大佬群 QQ 追番群

最后编辑于: 2020 年 06 月 11 日
返回文章列表 文章二维码
本页链接的二维码
打赏二维码
0:00