IEEE 浮点表示
IEEE 浮点标准:V = (-1)s * M * 2E 表示一个浮点数:
- 符号(sign) s 决定
V
的正(s=0)或负(s=1),对于 0 后面会有说明 - 尾数(Mantissa) 二进制小数
- 阶码(Exponent) E 的作用是对浮点数加权,这个权重是 2 的 E 次幂
将浮点数的位分为 3 个部分:
- 1 位的符号位 s 表示 s
- k 位的阶码字段 exp = ek-1...e1e0 表示 E
- n 位的小数字段 frac = fn-1...f1f0 表示 M
以 C 语言为例,不同的精度下,s、exp、frac 有不同的位数:
单精度:
31 30 23 22 0
+---+---------+-----------------------------------------+
| s | exp | frac |
+---+---------+-----------------------------------------+
双精度:
63 62 52 51 32
+---+----------------+----------------------------------+
| s | exp | frac(51:32) |
+---+----------------+----------------------------------+
31 0
+-------------------------------------------------------+
| frac(31:0) |
+-------------------------------------------------------+
- float:s 1位、exp 的 k=8位、frac=23位,合计 32位
- double:s 1位、exp 的 k=11位、frac=52位,合计 64位
分类
以 C 语言单精度为例,根据 exp 存储的位的不同,所表示的浮点数可以分成 3 中不同的情况,而最后一种情况中情况分两个变种:
-
规格化
+-------------------------------------------------------+ | s | exp!=0 & exp!=255 | frac | +-------------------------------------------------------+
这是最常见的情况,exp 的位模式既不为全 0,也不为全 1
规格化的值有两点需要特别注意:-
阶码 E 包含一定的偏置 Bias,也就是说
E = exp - Bias
,exp 是无符号数,Bias = 2k-1 - 1,偏置的作用是为了在规格化取值范围与非规格化取值范围之间平滑过渡 -
尾数 M 的值并不是 frac 所表示的小数值,实际情况是
M = 1 + frac
通常情况下,二进制整数部分通过调整小数点(也就是修改 E)来变成 1,所以 IEEE 的表示法直接将这一位的1
省去,这样二进制小数部分就能多存储一位,提高了精度,也就是说这个 frac 隐含了开头的 1举个例子:假设 frac 有 5 位,现在要存储一个二进制数 b,b 的值是 0.101011(2),调整一下权重:1.01011 * 2-1(2),farc 存储的就是小数点后面的这 5 位
01011
-
-
非规格化
+-------------------------------------------------------+ | s | exp=0 | frac | +-------------------------------------------------------+
exp 位模式全为 0,E = exp - Bias,M = frac
规格化数因 frac 隐含开头的 1,M >= 1,故而无法表示 0 这个数。
当 exp 和 frac 都是 0 时,s = 1 得到 -0.0,s = 0 得到 +0.0 -
- 无穷大(Infinity)
exp 所有位皆是 1,frac 所有位皆是 0 ,表示无穷大+-------------------------------------------------------+ | s | exp=255 | frac=0 | +-------------------------------------------------------+
s = 1 时表示负无穷大,s = 0 时表示正无穷大 - NaN(Not a Number)
exp 所有位皆是 1,frac 的位模式不全为 0 ,表示 NaN 不是一个数+-------------------------------------------------------+ | s | exp=255 | frac!=0 | +-------------------------------------------------------+
- 无穷大(Infinity)
阶码的值决定了该浮点数是规格化的、非规格化的、无穷大或者 NaN
转换示例
举例,将 1 个 float 数据转换为 4Byte 的二进制数据存储起来:
float a = 10.25F;
Decimal Binary
整数部分: 10 ====> 1010
小数部分: 0.25 ====> 0.01
科学计数法: 1.025 * 10^1 ====> 1010.01 = 1.01001 * 2^3
s = 0 ====> 0
E = 3 ====> E 包含偏置,IEEE 用 0111 1111 = 2^7 - 1 = 127 来表示 E = 0,所以当 E = 3 时,二进制表示为: 1000 0010 = 130 = 127 + 3
M = 1.01001 ====> 小数点前是 1,所以直接去掉,只保留小数部分,二进制表示为:0100 1000
s exp frac
10.25 ====> 0 | 1000 0010 | 01001000000000000000000
====> 0100 0001 0010 0100 0000 0000 0000 0000
需要注意的 3 个地方:
-
小数部分:0.25 = 1/4,即 2 的 -2 次方,也就是二进制的 1 小数点左移 2 位,所以得到的二进制表示为 0.01。需要注意的是:不是所有的十进制小数都能转换为二进制小数,比如十进制的 0.3,所以只能取近似值,通过 2-n(n>0,n 为整数) 来接近 0.3
-
规格数:基数为 2,位数最高位为 1 的数为规格数,此时能够表示的数据精度最高。通过阶码的大小来控制小数点的位置使得尾数变为规格数的过程,称为规格化。在存储时尾数只保存了小数部分。但是规格数无法表示 0。
-
阶码是有偏置的:2k-1 - 1
Java 中的应用
将给定的字节数组转换为对应的浮点数,JDK 中 Float 和 Double 均提供了对应的方法来处理这种情况,以 Float 为例,使用的方法java.lang.Float#intBitsToFloat(int bits)
,将参数 bits 的位模式解析为浮点数,API 中的说明:
int s = ((bits >> 31) == 0) ? 1 : -1;
int e = ((bits >> 23) & 0xff);
int m = (e == 0) ?
(bits & 0x7fffff) << 1 :
(bits & 0x7fffff) | 0x800000;
浮点结果等于算术表达式 s·m·2e-150 的值
对应给定的浮点数,进行四舍五入可以使用java.math.BigDecimal#setScale(int, int)
方法,可以设定保留小数的位数,以及四舍五入的方式
class Scratch {
public static void main(String[] args) {
int intbis = 0B0100_0001_0010_0100_0000_0000_0000_0000;
System.out.println(Integer.toBinaryString(intbis));
System.out.println(Float.intBitsToFloat(intbis));
System.out.println(Integer.toBinaryString(Float.floatToIntBits(10.25F)));
}
}
参考
- 浮点数在计算机中存储方式 - Robin Zhang - 博客园
- 深入理解计算机系统(2.7)---二进制浮点数,IEEE标准(重要) - 左潇龙 - 博客园
- 从JDK源码角度看Float - 掘金
- JDK API
- 计算机组成原理 第 2 版 P-229
- CSAPP 中文 第 3 版P-78
- 你应该知道的浮点数基础知识 • cenalulu's Tech Blog update 2019-11-14 四 01:27 下午 图画的很好