7

很抱歉,因为sf不支持行内公式,所以只能使用行间公式,导致格式有点难看。

引言

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

下面是浮点表示法

SignExponentFraction
(e+f)(e+f-1)......f(f-1)................0

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

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

    分类

clipboard.png

常规最大值和最小值

IEEE754标准中定义了下面的值

$$ emax=2^{w-1}-1 $$

$$ emin=1-emax=2-2^{w-1} $$

$$ p = t+1 $$

由于指数可能为正数,也可能为负数,为了方便表示,引入了一个偏差值

$$ Ebias = 2^{e-1}-1 $$

假设把我们要表示的浮点值改写成这样的二进制格式

$$ (-1)^{S}\times1.M_{0}M_{1}M_{2}...M_{22}\times2^{N} $$

那么表示成浮点数就是

SignExponentFraction
SN+Ebias$$M_{0}M_{1}M_{2}...M_{22}$$

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

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

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

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

Sign8-Bit Based Exponent23-Bit Normalized Fraction
[31][30:23][22:0]

取w=8; t=23;
所以Ebais = 2^{8-1}-1 = 127
N的取值范围是 (-2^{8-1}+1) - (2^{8-1}), 即-127 -- 128
做为常规值, N的取值范围是-126 -- 127
最大值和最小值为

MinNormMaxNorm
$$(-1)^{S} \times 2^{-126} \times 1.0$$$$(-1)^{S} \times 2^{127} \times (2-2^{-23}) $$
S 00000001 00000000000000000000000S 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)}

为了解决上面的问题,引入了非规格化浮点数。
IEEE754规定当指数N是-2^{e-1}+1(此时E=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)$$

无穷大

产生无穷的一般情形有:

  • 自身运算, 如负无穷+2.0得到负无穷
  • 被0除, 例如1/0得到正无穷
  • 上溢, 即计算结果超出类型范围,通过舍入得到无穷

NaN

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

正零和负零

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


计算异常

clipboard.png


和十进制间的转换

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

  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}$$
  • 第四步
    拼接结果如下

    符号指数尾数
    01000_01011001_0001_0000_0000_0000_000

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

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

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

以1 10001000 10011111110000000000000为例

符号指数尾数
11000_10001001_1111_1100_0000_0000_000

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

$$(-1 \times 2^{9} \times 1.10011111111)_{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(错误)。


harriszh
338 声望131 粉丝

做些有趣的事,留些有用的存在