浮点数在计算机内部表示原理
Contents
科学计数法
浮点数在计算机表示中, 是以科学计数法来表示的.
$$ -4.86 * 10^{11} \text { } = -4.86 * e11 $$
$$
即 \text { } A * 10^N
\text { } 等同于 \text { }AeN
$$
其中 A
满足 1<=|a|<10
一个科学计数法, 由三部分组成, 以上面的例子为例
符号位 | 有效数字 | 指数 |
---|---|---|
- | 4.86 | 11 |
有效数字 : 从左边第一个非零数字开始的全部数字
十进制的科学计数法中, 有效数字的整数部分必须在
[1, 9]
区间, 满足这条件的, 称为规格化
单精度浮点数的表示
浮点数表示标准 IEEE754 维基百科
取值范围
精度 | 字节数 | 正数范围 | 负数范围 |
---|---|---|---|
单精度 | 4 | 1.4e-45 ~ 3.4e38 | -3.4e38 ~ -1.4e-45 |
双精度 | 8 | 4.9e-324 ~ 1.798e308 | -1.798e308 ~ -4.9e-324 |
单精度的二进制内部表示格式: 共32位, 最右边是第1(或0)位, 最左边是第32(或31)位
0(符号位, 1位)|00000000(指数位, 8位)|00000000000000000000000(有效数字位, 23位)
概念
符号位
: 用于存储符号(正或负).0
表示正数.1
表示负数阶码位
: 即上面的指数位. 符号位右边8位. 注意, IEEE754标准规定这里存储的是指数对应的移码
尾数位
: 即上面的有效数字位
阶码位
移码: 将一个真值在数轴上正向平移一个偏移量后得到的. 即 $$ [x]_移 = x + 2^{n-1} $$ (n 为二进制位数, 含符号位). 它的几何意义是把真值映射到一个正数域, 其特点是可以直观地反映两个真值的大小, 即移码大的真值也大.
指数与阶码之间的换算关系, 就是指数与它的移码之间的换算关系. 假设指数的真值为e, 阶码为E. 则有
$$
E = e + (2^{n-1} - 1)
$$
其中的 2^(n-1) -1
就是 IEEE754标准规定的偏移量(在这里是 127 ). n=8
是阶码的二进制位数.
为什么偏移量是 2^(n-1) -1
呢?
8个二进制能表示的指数取值范围为 [-128, 127]
, 变成移码的话, 即将该区间正向平移到正数域, 即 +128
, 得到阶码的范围为 [0, 255]
. 但计算机规定, 阶码全为0或全为1两种情况会当作特殊值处理(全为0是机器0, 全为1是无穷大). 除去这两个值, 则阶码的范围为 [1, 254]
. 这样子, 如果偏移量为 128, 则根据移码公式得到的指数范围为 [-127, 126]
, 指数最大为126, 这会缩小浮点数能表示的范围, 所以规定了单精度的移码偏移量为 2^(n-1) - 1
, 即 127. 这样, 指数的范围就是 [-126, 127]
尾数位
IEEE754规定, 尾数以原码表示. 正指数和有效数字的最大值, 决定了32位存储空间能够表示浮点数的十进制最大值. 指数最大值为 $$ 2^{127} 约等于 1.7 * 10^{38} $$
严格来说是
1.70141183e38
有效数字部分的最大值是二进制的 1.11111111111111111111111
(小数点后23个1). 是一个无限接近2的数字, 所以得到的最大的十进制数为
$$
2 * 1.7 * 10^{38} 约等为 3.4e38
$$
二进制的规格化后的尾数形式为 1.xyz
满足 1 <=|a|<2
. 为了节约存储空间, 将符合规格化尾数的首个1省略. 所以, 尾数表面上是23位, 却表示了24位二进制数.
浮点数转换为二进制表示
System.out.println(Integer.toBinaryString(Float.floatToIntBits(16.35F)));
//0|10000011|00000101100110011001101
注意, 上面将二进制表示的形式再转换为十进制后与真值不一样. 上面的有效数字为 1.00000101100110011001101
转换为十进制后为
$$
1 + 2^{-6} + 2^{-8} + 2^{-9} + 2^{-12} + 2^{-13} + 2^{-16} + 2^{-17} + 2^{-20} + 2^{-21} + 2^{-23} = 1.021875023841858
$$
$$ 1.021875023841858 * 2^4 = 16.350000381469727 $$
注意, 单精度十进制的最大有效数字是7位( 2^23 = 8388608, 共7位数 ). 所以 1.021875023841858
显示为 1.021875
是有效的.
加减运算
步骤为
- 零值检测. (0有浮点数中是一种规定, 即阶码与尾数全为0). 如果有一个数为0, 可直接得出结果
- 对阶操作. 比较阶码的大小判断小数点位置是否对齐. 阶码不相等时, 表示没有对齐, 则需要通过移动尾数改变阶码的大小, 使地者最终相等. 这个过程就是
对阶
. 尾数向右移1位, 阶码值加1. 反之减1.移动尾数时, 部分二进制位将被移出, 但向左移会使高位被移出, 对结果造成的误差更大. 所以 IEEE754规定,对阶操作的移动方向为向右移
, 即选择阶码小的数进行操作 - 尾数求和. 对阶完成后, 直接按位相加即可完成求和(如果是负数, 则要先转换为补码再进行运算). 这跟十进制加法相同, 指数大小对齐后, 直接相加.
- 结果规格化. 通过尾数位的向左或右移动调整达到规格化形式. 向右移称为右规, 反之为左规.
- 结果舍入. 上面的对阶中尾数移出的数据会被保存起来, 称为
保护位
, 等到规格化为再根据保护位进行舍入处理.
例子 1.0F - 0.9F
System.out.println(1.0F - 0.9F);
//0.100000024
1.0 的二进制为
0|01111111|00000000000000000000000
-0.9 的二进制为
1|01111110|11001100110011001100110
浮点数 | 符号 | 阶码 | 尾数 | 尾数补码 |
---|---|---|---|---|
1.0 | 0 | 127 | (1)000-0000-0000-0000-0000-0000 | (1)000-0000-0000-0000-0000-0000 |
0.9 | 1 | 126 | (1)110-0110-0110-0110-0110-0110 | (0)001-1001-1001-1001-1001-1010 |
计算步骤
对阶
. 1.0 阶码为 127, -0.9阶码为126. 所以 将-0.9尾数的补码向右移到, 使其阶码为 127, 同时高位要补1, 移动后尾数为 1000-1100-1100-1100-1100-1101
尾数求和
. 因为尾数都转换为补码, 所以直接按位相加. 注意, 符号位也要参与运算.
数字 | 符号位 | 尾数位 |
---|---|---|
1.0 | 0 | (1)000-0000-0000-0000-0000-0000 |
-0.9 | 1 | (1)000-1100-1100-1100-1100-1101 |
结果 | 0 | (0)000-1100-1100-1100-1100-1101 |
规格化
. 上面的结果并不符合规格化要求. 规格化的尾数最高位必须是1, 所以, 需要将尾数结果向左移动4位, 同时阶码需要减4. 移动后, 阶码为123 (127 - 4), 二进制为 01111011
. 尾数为 1100-1100-1100-1100-1101-0000
即最后结果为
符号位 | 阶码 | 尾数 |
---|---|---|
0 | 01111011 | (1)100-1100-1100-1100-1101-0000 |
尾数是23位的, 第24位是隐藏位
. 结果对应的十进制即为:
1111011
=> 123. 123 - 127 = -4
(1)100-1100-1100-1100-1101-0000
十进制为
$$
(因为尾数是 1.xyz) \text { } 1 +2^{-1}+2^{-4}+2^{-5}+2^{-8}+2^{-9}+2^{-12}+2^{-13}+2^{-16}+2^{-17}+2^{-19} = 1.6000003814697266
$$
结果为
$$
1.6000003814697266 * 2^{-4} = 0.10000002384185791
$$
在规格化为显示为 0.100000024
说明
以上内容, 是自己读 <码出高效 Java开发手机> 摘抄 + 自己理解的.