科学计数法

浮点数在计算机表示中, 是以科学计数法来表示的.

$$ -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是有效的.

加减运算

步骤为

  1. 零值检测. (0有浮点数中是一种规定, 即阶码与尾数全为0). 如果有一个数为0, 可直接得出结果
  2. 对阶操作. 比较阶码的大小判断小数点位置是否对齐. 阶码不相等时, 表示没有对齐, 则需要通过移动尾数改变阶码的大小, 使地者最终相等. 这个过程就是 对阶. 尾数向右移1位, 阶码值加1. 反之减1.移动尾数时, 部分二进制位将被移出, 但向左移会使高位被移出, 对结果造成的误差更大. 所以 IEEE754规定, 对阶操作的移动方向为向右移, 即选择阶码小的数进行操作
  3. 尾数求和. 对阶完成后, 直接按位相加即可完成求和(如果是负数, 则要先转换为补码再进行运算). 这跟十进制加法相同, 指数大小对齐后, 直接相加.
  4. 结果规格化. 通过尾数位的向左或右移动调整达到规格化形式. 向右移称为右规, 反之为左规.
  5. 结果舍入. 上面的对阶中尾数移出的数据会被保存起来, 称为 保护位 , 等到规格化为再根据保护位进行舍入处理.

例子 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开发手机> 摘抄 + 自己理解的.