再谈浮点数
Posted sinkinben
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了再谈浮点数相关的知识,希望对你有一定的参考价值。
写在前面:本文要求对浮点的编码表示已有一定基础,如果对「IEEE 754」这一个词组不熟悉,请勿继续阅读,以免浪费时间。
重温 ICS,又学习了一遍计算机中的浮点数的编码表示,似乎又有了一些新的理解。
先来复习一下基本知识:对于一个浮点数,计算机采用「科学计数法」去表示:
[
value = (-1)^S cdot M cdot 2^{E}
]
(S) 表示这个数的符号。其中 (M) 总是为 1.xxxxx
这种形式( x
代表 0/1
),(E) 是指数。
好了,下面正式「复习」IEEE 754 浮点数标准。
浮点数编码
在 IEEE 754 标准当中,float
采用 32 bit 去保存,double
采用 64 bit 保存,格式如下:
float
---------------------------
| S | exp | frac |
| 1 | 8 | 23 |
---------------------------
double
----------------------------
| S | exp | frac |
| 1 | 11 | 52 |
----------------------------
exp
为指数,frac
为尾数,S
为符号位。其数值表示:(value = 1.frac imes 2^{E} imes(-1)^{S}) 。其中,(E) 与 exp
相关,二者的值并不相等(什么关系可以看下面)。
这里为什么是 1.frac
呢?因为对于任意的二进制小数表示,总是可以化为 (1.frac imes 2^{E}) 的形式。例如,(10.01 = 1.001 imes 2^1) ,(0.000101 = 1.01 imes 2^{-4}) 。所以只需要记录 小数点后的尾数 即可(节省了 1 bit ,这也体现了计算机系统的设计哲学:尽最大努力进行优化)。
对于 (1.0101 imes 2^{-4}) ,易知 frac = 0101
,需要注意的是,在内存中,frac
的域从高位到低位,分别是 0101 0000 0000 0000 0000 000
。不足 23 位,后面的所有 bit 设置为 0 。
值得重新「复习」的是 exp
这一部分。
对于 exp
来说,任何时候我们都把解释为无符号数,并且保留 2 种特殊情况:
exp == 0x00
:表示非规格化浮点数 (Denormalized Number) 。exp == 0xff
:表示 2 种特殊值(后面会进一步陈述)。
8 bit 的无符号数,去除最大的全 0 ,和最小的全 1 ,其取值范围为 ([1,254]) 。显然,这无法表示 (1.01 imes 2^{-4}) 这种负指数的情况。因此,在 ([1,254]) 区间内,需要对这些数值平均分配,做一次映射,一半表示负指数,一半表示正指数,因此有:
[
E = exp - 127
]
为什么是 127 呢?因为 ([1,254]) 有 254 个数,127 是 254 的一半,刚好一半表示负指数,一半表示正指数。因此 (E) 的取值范围为:
[
E in [-126, 127]
]
127 就是所谓的偏置常数 (bias) ,其数值由下面公式计算所得:
[
bias = 2^{k-1} - 1
]
其中 (k) 是 exp
的比特位数。因此 float
的 (bias) 是 127 ,double
的 (bias) 是 1023 。
综上所述,对于一个规格化的比特串,应采用下面的公式转换为二进制表示的小数:
[
value = 1.frac imes 2^{exp-bias} imes (-1)^{S}
]
Aside
为什么不把exp
看作是有符号数?这样采取补码的方式去记录,不就能直接表示正负指数?这是为了方便在硬件层面比较
float
和double
。对于正浮点数a
和b
,就可以从高位到低位进行比较,规则与unsigned int
一模一样,如果存在bit(a,i) > bit(b,i), i=31...0
,那么就说明 (a>b) 。如果将
exp
采用补码的方式,那么在比较 2 个浮点数时,除了要比较float
的符号位,还需要对exp
的符号位进行比较。
考虑到比特串 0x00000000
,可以得到 s = 0, exp = 0x00, frac = 0...0
,如果不考虑非规格化数的特殊规则 ,即不保留 exp = 0x00
这种特殊情况,(E = exp - 127 = -127) ,对应的数值应当为 (1.0 imes 2^{-127}) ,但很不幸,这应当是 +0
的编码。因此,我们才需要引入非规格化数,并且重新解释 exp
和 frac
。换句话说,如果我们不引入非规格化数,我们就无法在 float
中表示数值 0
,因为我们的 frac
总是隐含为 1.frac
。
Aside
为什么要强调是+0
呢?因为在「IEEE 754」标准当中,
+0
和-0
虽然都是0
,但在科学计算的某些特殊场合,它们是表示不同的含义的,因此给0
保留了这 2 种编码(可参考 CSAPP 的 2.4 小节)。
好了,我们回到 exp
的特殊情况:
exp = 0x00
:表示非规格化浮点数此时,(E = 1 - bias) ,而不是 (0-bias) 。
frac
应该解释为0.xxxxx
这种形式,而不是1.xxxxx
(后面解释原因)。实质上相当于小数点左移一位 ,指数exp
加一作为「补偿」。exp = 0xff
:表示 2 种特殊值- 当
frac = 0
时,表示 (+infty) 或者 (-infty) ,与之做运算会 overflow 。例如,1.0/0.0
和-1.0/0
属于这种情况。 - 当
frac != 0
时,表示 NaN (Not A Number) 。例如,(sqrt(-1), infty imes 0, infty - infty) 属于此类情况。
- 当
引入「非规格化数」之后,就解决了 0
的编码问题。对于 0x00000000
,可得 exp = 0x00, frac = 0...0
,因此 (E=1-127=-126) ,所以 (val = 0.frac imes 2^E = 0.0...0 imes 2^{-126} = 0) 。
实际上,「非规格化数」的引入还有一个好处:实现最大非规格化数到最小规格化数的平滑转变 。
在 32 位 float
中,显然非规格化数的所有编码为:
s exp frac
0 0000 0000 0000 0000 0000 0000 0000 000 => 0
0 0000 0000 0000 0000 0000 0000 0000 001 => 0.00000000000000000000001
0 0000 0000 0000 0000 0000 0000 0000 010 => 0.00000000000000000000010
...
0 0000 0000 1111 1111 1111 1111 1111 111 => 0.11111111111111111111111
最低位不断加 1 ,对应的数值变化是 (frac{1}{2^{23}}) ,这就使得非规格化数在 0 的附近是均匀分布的,具有 (gradual quad underflow) 的性质(后续我打算做一个图示来说明为什么引入「非规格化数」之后,float
的数值变化会更平滑)。
以上是关于再谈浮点数的主要内容,如果未能解决你的问题,请参考以下文章
OpenGL:为啥我不能将单个浮点数从顶点着色器传递到片段着色器?