IEEE754浮点表示法详解

c/c++

浏览数:91

2019-3-30

引言

目前流行都是上层的语言和框架,通常情况下其实我们并不需要去了解底层实现。但有时候我们会遇到一些奇怪的错误,不了解底层实现的话就无法想通。
比如下面一个C的例子

#include <stdio.h>
int main(int argc, char** argv)
{
    int num=8;
    float* pfnum = &num;
    printf("num = %d\n", num);
    printf("*pfnum = %f\n", *pfnum);
    *pfnum = 8.0;
    printf("num = %d\n", num);
    printf("*pfnum = %f\n", *pfnum);
    return 0;
}

输出结果为

num = 8
*pfnum = 0.000000
num = 1090519040
*pfnum = 8.000000

另外一个有趣的应用是计算2的74次方,很明显64位系统上只能表示到2的64次方
但下面的例子可以得到

#include <stdio.h>
#include <math.h>

int main(int argc, char** argv)
{
    printf(" num = %f\n", pow(2, 74));
    return 0;
}

输出是

num = 18889465931478580854784.000000

要理解以上问题,那我们就需要对浮点在底层的实现有一定了解

总述

IEEE754是IEEE二进制浮点算术标准。这个标准定义了表示浮点数的常规值与非规格化值(denormal number),一些特殊值(infinity)和非数值(NaN), 以及这些数值的浮点运算。另外它还规定了运算结果的近似原则和例外状况(包括例外发生的时机和处理方式).
虽然IEEE754只定义了单精度(32位),双精度(64位),扩展单精度(43位以上),与扩展单精度(79位以上)。但实现上它的定义法可以扩展到任意精度。所以下面的公式尽量针对任意精度。

格式

作为对比,我们先列出实数表示法

msb lsb
n-1 …………………… 0

下面是浮点表示法

Sign Exponent Fraction
(e+f) (e+f-1)……f (f-1)…………….0

从上面表格可以看出浮点由三部分组成:

  • (e+f)位 : 表示浮点的符号位
  • (e+f-1)……f 位 : 表示浮点的指数域
  • (f-1)……0 位 : 表示浮点的尾数域

分类

符号位 指数位 尾数 隐含位 意义 说明
0/1 0 0 1 $\\\pm0$ 尾数全0
0/1 0 非0 非规格化数 尾数非全0
0/1 $1$到$(2^{e}-2)$ 任意值 1 常规值
0/1 $2^{e}-1$ 0 1 $\\\pm$无穷大 尾数全0
0/1 $2^{e}-1$ 1xx…xxx 1 QNaN 尾数最高位为1,尾数不为0
0/1 $2^{e}-1$ 0xx…xxx 1 SNaN 尾数最高位为0,尾数不为0

常规最大值和最小值

由于指数可能为正数,也可能为负数,为了方便表示,引入了一个偏差值$$Ebias = 2^{e-1}-1$$
假设把我们要表示的浮点值改写成这样的二进制格式
$$(-1)^{S}\times1.M_{0}M_{1}M_{2}…M_{22}\times2^{N}$$
那么表示成浮点数就是

Sign Exponent Fraction
S N-Ebias $M_{0}M_{1}M_{2}…M_{22}$

N-Ebias一共有e位,那么它们的取值范围是$$0 \rightarrow (2^{e}-1))$$
其中$Ebias = 2^{e-1}-1$
N的取值范围是$$(-2^{e-1}+1) \rightarrow (2^{e-1})$$
$-2^{e-1}+1$ 和 $2^{e-1}$ 被保留,用来表示一些特殊值和非数值。

所以做为常规值, N的取值范围是$$(-2^{e-1}+2) \rightarrow (2^{e-1}-1)$$

MinNorm MaxNorm
$$(-1)^{S} \times 2^{-2^{e-1}+1} \times 1.0$$ $$(-1)^{S} \times (2-2^{-f}) \times 2^{2^{e-1}-1} $$

上面公式可能有点绕,不过如果代入具体数值就比较好理解了。 我们以单精度为例

Sign 8-Bit Based Exponent 23-Bit Normalized Fraction
[31] [30:23] [22:0]

$Ebais = 2^{8-1}-1 = 127$
N的取值范围是 $(-2^{8-1}+1) – (2^{8-1})$, 即-127 — 128
做为常规值, N的取值范围是-126 — 127

最大值和最小值为

MinNorm MaxNorm
$$(-1)^{S} \times 2^{-126} \times 1.0$$ $$(-1)^{S} \times (2-2^{-23}) \times 2^{127} $$
S 00000001 00000000000000000000000 S 11111110 11111111111111111111111

非规格化数

对于单精度值,有两数$1.001 \times 2^{-125}$ 和$1.01 \times 2^{-125}$, 它们的差值是$0.001 \times 2^{-125} = 1.0 \times 2^{-128}$
-128超过了我们常规值允许的最小值-126.
如果近似为0, 那么下面的公式就会出问题

if(x != y) { z = 1/(x-y)}

为了解决上面的问题,引入了非规格化浮点数。
它规定当指数是$-2^{e-1}+1$时,尾数不必是规范化的。
这时指数的移码就是0, 它的尾数域不在隐藏一个1.
把上面的差表示成$0.01 \times 2^{-126}$, 它的二进制浮点数为0_00000000_01000000000000000000000.
注意 非规格化数没有隐藏位,或者是可以看隐藏位是0
它可以表示成
$$\\\pm(f) \times 2^{0-Ebias}$$
f的取值范围是(0,2)
IEEE754标准中对它的值定义为:
$$v=(-1)^s \times 2^{emin} \times (0+2^{-t} \times M)$$

无穷大

产生$\\\infty$的一般情形有:

  • 自身运算, 如-$\\\infty+2.0$得到-$\\\infty$
  • 被0除, 例如1除以正0得到+$\\\infty$
  • 上溢, 即计算结果超出类型范围,通过舍入得到$\\\infty$

NaN

QNaN(Quiet NaN): 参与运算不触发异常
SNaN(signal NaN): 参与运算触发异常
IEEE引入NaN的目的是给compiler等系统一个约定的值未初始化的数据,或者在计算出问题时可以返回一个值来提示计算出问题了。

正零和负零

零有正负之分,非常容易让人困惑。这主要是基于数值分析后的权衡结果。
IEEE规定+0 == -0.
比如,如果零无符号, 则等式1/(1/x) == x 在x=$\\\infty$(无论是正无穷还是负无穷)时不再成立。
原因是 1除-$\\\infty$都等于0, 1除以0等于+$\\\infty$,与x不相等。要解决这个问题的一个方法是$\\\infty$也无符号,但+$\\\infty$和-$\\\infty$显然分布在轴的两侧,可以表示上溢和下溢发生在哪一侧,所以不能不要。
当然零有符号也造成了一些问题,比如当x=y时, 1/x=1/y在x和y分别为+0和-0时,不再成立。解决这个问题的方法是规定零是有序的,即+0不等于-0, 但如果这样的话,即使if(x==0)这样简单的判断也会由于x可能是$\\\pm0$而变得不确定。所以两害取其轻,零还是无序问题少一点。

计算异常

  • 上溢 = 正无穷大
  • 无穷大 + 任何数 = 无穷大
  • 任何有限数 $\\\div$ 0 = 无穷大
  • 任何有限数 $\\\div$ 无穷大 = 0
  • 0/0 = NaN
  • 无穷大 $\\\div$ 无穷大 = NaN

和十进制间的转换

十进制浮点数到二进制浮点数的转换

  1. 十进制到二进制: 整数部分用2来除,小数部分用2来乘
  2. 规格化二进制数: 改变小数点,使小数点前只有第一位有效数字
  3. 计算指数的移码:原来的指数+$2^{e-1}-1
  4. 把符号位,指数的移码,尾数合在一起 (尾数不够补0)

以100.25变例

  • 第一步

    100/2 = 50 ... 0
    50/2  = 25 ... 0
    25/2  = 12 ... 1
    12/2  = 6  ... 0
    6/2   = 3  ... 0
    3/2   = 1  ... 1
    1/2   = 0  ... 1      // 得到0,  停止
    
    0.25 * 2 = 0.5 ... 0
    0.5  * 2 = 1.0 ... 1     // 得到1.0, 停止

    $$(100.25)_{10} = (1100100.01)_{2}$$

  • 第二步
    $$1100100.01 = (-1)^0 \times 1.10010001 \times 2^{6}$$
  • 第三步
    指数的移码:$$(6+127)_{10} = (133)_{10} = (85)_{16}$$
  • 第四步
    拼接结果如下
符号 指数 尾数
0 1000_0101 1001_0001_0000_0000_0000_000

二进制浮点数到十进制浮点数的转换

过程和十进制到二进制相逆

  1. 分割符号位, 指数移码, 尾数
  2. 将移码送去偏移$2^{e-1}-1$, 得到真正的指数
  3. 写成规格化的二进制浮点数
  4. 写成非规格化的二进制浮点数形式
  5. 把二进制数转换成十进制数

以1 10001000 10011111100000000000000为例

符号 指数 尾数
1 1000_1000 1001_1111_1000_0000_0000_000

$$(10001000)_{2} – (127)_{10} = (136)_{10} – (127)_{10} = (9)_{10}$$

$$(-1 \times 2^{9} \times 1.100111111)_{2} = (-1 \times 1100111111.1)_{2} = (-831.5)_{10}$$

精度取舍规则

简明口诀:「4舍6入5看右,5后有数进上去,尾数为0向左看,左数奇进偶舍弃」。
为了避免四舍五入规则造成的结果偏高,误差偏大的现象出现,一般采用四舍六入五留双规则。
  
当尾数小于或等于4时,直接将尾数舍去
例如将下列数字全部修约到两位小数,结果为:
10.2731——10.27
18.5049——18.50
16.4005——16.40
27.1829——27.18
当尾数大于或等于6时将尾数舍去向前一位进位
例如将下列数字全部修约到两位小数,结果为:
16.7777——16.78
10.29701——10.30
21.0191——21.02
(三)当尾数为5,而尾数后面的数字均为0时,应看尾数“5”的前一位:若前一位数字此时为奇数,就应向前进一位;若前一位数字此时为偶数,则应将尾数舍去。数字“0”在此时应被视为偶数。
例如将下列数字全部修约到两位小数,结果为:
12.6450——12.64
18.2750——18.28
12.7350——12.74
21.845000——21.84
(四)当尾数为5,而尾数“5”的后面还有任何不是0的数字时,无论前一位在此时为奇数还是偶数,也无论“5”后面不为0的数字在哪一位上,都应向前进一位。
例如将下列数字全部修约到两位小数,结果为:
12.73507——12.74
21.84502——21.85
12.64501——12.65
18.27509——18.28
38.305000001——38.31
按照四舍六入五留双规则进行数字修约时,也应像四舍五入规则那样,一次性修约到指定的位数,不可以进行数次修约,否则得到的结果也有可能是错误的。例如将数字10.2749945001修约到两位小数时,应一步到位:10.2749945001——10.27(正确)。如果按照四舍六入五留双规则分步修约将得到错误结果:10.2749945001——10.274995——10.275——10.28(错误)。