很抱歉,因为sf不支持行内公式,所以只能使用行间公式,导致格式有点难看。
引言
目前流行都是上层的语言和框架,通常情况下其实我们并不需要去了解底层实现。但有时候我们会遇到一些奇怪的错误,不了解底层实现的话就无法想通。
比如下面一个C的例子
#include <stdio.h>
int main(int argc, char** argv)
{
int num=8;
float* pfnum = #
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 位 : 表示浮点的尾数域
-分类
常规最大值和最小值
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} $$
那么表示成浮点数就是
Sign | Exponent | Fraction |
---|---|---|
S | N+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)
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] |
取w=8; t=23;
所以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^{127} \times (2-2^{-23}) $$ |
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)}
为了解决上面的问题,引入了非规格化浮点数。
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可能是正负零而变得不确定。所以两害取其轻,零还是无序问题少一点。
计算异常
和十进制间的转换
十进制浮点数到二进制浮点数的转换
- 十进制到二进制: 整数部分用2来除,小数部分用2来乘
- 规格化二进制数: 改变小数点,使小数点前只有第一位有效数字
- 计算指数的移码:原来的指数加上2^{e-1}-1
- 把符号位,指数的移码,尾数合在一起 (尾数不够补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
二进制浮点数到十进制浮点数的转换
过程同十进制到二进制相逆
- 分割符号位, 指数移码, 尾数
- 将移码减去偏移2^{e-1}-1, 得到真正的指数
- 写成规格化的二进制浮点数
- 写成非规格化的二进制浮点数形式
- 把二进制数转换成十进制数
以1 10001000 10011111110000000000000为例
符号 | 指数 | 尾数 |
---|---|---|
1 | 1000_1000 | 1001_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(错误)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。