浮点数原理
浮点数的作用:区别于整形数,用来表示小数。可以用来表示很大的数,或者非常接近 0 的小数,或者近似的做实数计算,浮点数的一般形式:
IEEE(pronounced “Eye-Triple-Eee”)浮点数标准 是行业内公认的标准。
rounding:when a number cannot be represented exactly in the format and hence must be adjusted upward or downward。可以翻译为:舍入。
十进制的小数表示:
相应的,二进制也可以写成这种形式:
浮点数的表示
IEEE 浮点数的格式:
- s 是符号(Sign),s 为 0 时是正,s 为 1 时是负
- M 是有效数字(Significand,即 尾数)
- E 是 指数,Exponent,也叫 幂数,阶码
- 隐含的 基数 是 2
下图是浮点数的内存分布模型,首先是符号域,然后是指数域,最后是分数域:

- 符号位 s 个,符号位只需要一位,s=1
- 指数位 k 个,指数域
,用来计算指数 E - 分数为 n 个,分数域
,用来计算有效数字 M
32 位浮点数(单精度,float 型)中,s=1,k=8,n=23;64 位浮点数(双精度,double 型)中,s=1,k=11,n=52。
正常化值(Normalized Values)
当
-126 ~ +127,双精度的时候是:-1022 ~ +1023,其实
非正常化值(Denormalized Values)
当指数域全 0,就是非正常化格式。
在这种情况下,指数值是
+0.0,当符号位是 1 的时候就是-0.0。
除了可以表示 0,这个格式的另一个作用就是用来表示非常接近 0 的数。
特殊值(Special Values)
当指数域全 1 的时候,且分数域是全 0,就表示无穷大,如果符号域为 0,表示
当指数域全 1,且分数域并非全 0 的时候,结果可以叫做:NaN(Not a Number 的简写),这种值用来表示不能用实数或者无穷大表示的计算结果,比如计算:
综合理解
下图是三类浮点数在数轴上的显示:

可以看到非正常化值集中在 0 附近,正常化值散布在整个数轴的空间,特殊值则只表示两个无穷值。
下图是浮点数三种类型的光滑衔接:

看完浮点数的设计和构造我们可以发现以下这些特点:
- 从编码上有效数字域采用了无符号整数编码,而指数域采用了移码编码
- 非正常化值均匀分布在 0 附近
- 正常化值的间隔随着
变大而逐渐变大,也就是精度逐渐降低 - 精度是分组的,以
增加 1 为一组,每组有 个数(n 是有效数字域的位数) - 最高精度就是两个非正常化值的间隔,最低精度是最大的一组正常化值的相邻两数的间隔。
- 非正常化值按照精度只占一组,正常化值的数量是非正常化值数量的
倍 - 正常化值的第一组的精度和非正常化值的精度一样,也就是实现了无缝衔接
浮点数的计算
舍入

浮点数中使用的是:舍入到最近的偶数,因为舍入结果放大和缩小各占 50%的概率,这样就可以防止最终结果偏大或者偏小。
下面是把浮点数舍入到小数点后两位数:
浮点数加减运算
基本性质
- 相加可能产生 infinity 或者 NaN
- 不满足交换律,不满足结合律(因为舍入会造成精度上的损失)
- 加上 0 等于原来的数
- 除了 infinity 和 NaN,每个元素都有对应的相反数
- 除了 infinity 和 NaN,满足单调性,即
1 |
|
运行结果:
1 | 0 |
具体细节
设两个浮点数
则浮点数加减运算结果为:
- 对阶:首先要把指数位(阶码)调成一样,并相应的使 M 移位,由于有效域左移会引起最高有效位丢失,误差大,所以采用右移,此时阶码要增加。所以对阶原则是:小阶向大阶看齐。
- 有效数加减:简单的无符号数字相加减。
- 规格化:有效数求和结果可能大于 1,那么就向右规格化:尾数右移 1 位,阶码加 1。
- 舍入:对于右移出去的位,采取舍入
- 检查阶码是否溢出:
- 阶码下溢:运算结果为非规格化数
- 阶码上溢:置溢出标志
浮点数加减实例
首先算出
11,0.14 的二进制要稍微计算一下,我们让 0.14 不断的乘以 2(也就是左移),得到的整数位部分就是其二进制值的一位:
1 | 0.14 * 2 = 0.28 0 |
我们可以写个程序来完成这个计算工作:
1 |
|
上面代码保存成:float2Bitset.cpp文件,然后编译,并使用:
1 | $ g++ -o float2Bitset float2Bitset.cpp |
小数位精确到 23 位的话,3.14 的定点浮点数表示是:11.00100011110101110000101。
转成浮点数,首先规格化 M,那么整体要右移 1 位,指数是 1,由
1000 0000。
最终 3.14 的内存表示是:
同样的方法得到 2.718 的内存表示:
这两个数恰好是同阶的,那么就不需要对阶操作了。将 M 相加,但这个数太长了看着眼花,我们写个加法程序:
1 |
|
上述代码保存成:addBitset.cpp,编译并使用该程序:
1 | $ g++ -o addBitset addBitset.cpp |
相加结果等于:0 11101101110100101111001,最高位没有产生进位,这里用了一个 0 来代替,但两个前导 1 相加产生了进位,所以还需要对 M 右归一下,再对指数加 1。所以加法结果的浮点数表示是:
这个数的十进制表示的计算方法是:$$2^2 \times (1+0\times (\frac{1}{2})^1 + 1\times (\frac{1}{2})^2 + 1\times (\frac{1}{2})^3 +1\times (\frac{1}{2})^4+0\times(\frac{1}{2})^5+\cdots)$$
我们依然采用程序来计算这一长串二进制对应的十进制小数:
1 |
|
上述代码保存为:Bitset2float.cpp,编译并执行:
1 | $ g++ -o Bitset2float Bitset2float.cpp |
对得到结果:0.4645,
算法流程图

这个流程图并不是完美的,真实的浮点数流程图和浮点数计算电路比这个复杂。另外我忘画了一个东西,这个图最后应该加上溢出处理模块,E 可能会上溢(当 E 加 1 的时候),也可能会下溢(当 E 减 1 的时候)。
最后这个流程图中没有对特殊值的判断,比如:
了解了浮点数加法的流程之后,最后我们回到最上面说的 浮点数加减法不满足交换律和结合律,从计算细节分析为什么不行。
首先 3.14 的浮点数表示我们已经计算过了,那么 1e20 的浮点数是多少呢?1e20 也就是

很遗憾的是 64bit 只能摆的下
首先是 M 规格化,M 右移 63 位,E 加 63,舍入 M,那么 1e19 最终的双精度浮点数表示是:0 10000111110 0001010110001110010001100000100100010011110100000000
小阶向大阶看齐,3.14 的阶是 1,M 需要右移 62 位,而 M 的精度才 52,可想而知 M 就是 0 了。那么 3.14 + 1e19 的结果就是 1e19。1e20 就更加不用说了。
浮点数乘除
基本性质
- 相乘可能产生 infinity 或者 NaN
- 不满足交换律,结合律,分配率(因为溢出会造成程序无法计算出正确的结果)
- 乘以 1 会等于原来的数
- 除了 infinity 和 NaN,满足单调性:
具体细节
设两个浮点数
则浮点数乘除运算结果是:
- 计算阶码,判断是否溢出
- 求有效数的乘积
- 有效数舍入
- 计算符号位
浮点数还有相当多的细节,可以参考:IEEE 754