浮点数——科学计数法、浮点数表示、加减运算和浮点数的使用

目录

1.2浮点数

1.2.1 科学计数法

1.2.2 浮点数表示

1 符号位

2价码位

1.2.3 加减运算

1.2.4 浮点数的使用

 

1.2浮点数

浮点数是采用科学计数法来表示的,由符号位、有效数字、指数三部分组成。使用浮点数存储和计算的场景无处不在,若使

用不当则容易造成计算与理论值不一致,如下示例代码。

float a = 1f;
float b = 0.9f;


// 结果为: 0.100000024
float f = a -b;

执行结果显示计算结果与预期存在明显得误差,下面我们来深入剖析造成这个误差的原因来介绍浮点数的构成与计算原理。由于浮点数是以科学计数法来表示的。

1.2.1 科学计数法

浮点数是计算机用来表示小数的一种数据类型。在数学中,采用科学计数法来近似表示一个极大或一个极小且位数较多的数。例如a × 10^{n},其中a满足1≤ |a| <10,10^{n}是以10为底数,n为指数的幂运算表达式。a × 10^{n}还可以表示成aen。

科学计数法的有效数字为从第一个非零数字开始的全部数字,指数决定小数点的位置,符号表示该数的正负。值得注意的是,十进制科学计数法要求有效数字的整数部分必须在[1, 9]区间内,满足这个要求的表示形式被称为“规格化”。科学计数法可以唯一地表示任何一个数,且所占用的存储空间会更少,计算机就是利用这一特性表示极大或极小的数值。例如长整型能表示的最大值约为922亿亿,想要表示更大量级的数值,必须使用浮点数才可以做到。

1.2.2 浮点数表示

浮点数表示就是如何用二进制数表示符号、指数和有效数字。当前业界流行的浮点数标准是IEEE754,该标准规定了4种浮点数类型:单精度、双精度、延伸单精度和延伸双精度。前两种类型是最常用的,它们的取值范围如表1-4所示。

表 1-4 单精度和双精度
精度 字节数 正数取值范围 负数取值范围
单精度类型 4 1.4e-45 至 3.4e+38 -3.4e+38 至 -1.4e-45
双精度类型 8 4.9e-324 至 1.798e+308 -1.798e+308 至 -4.9e-324

因为浮点数无法表示零值,所以取值范围分为两个区间: 正数区间和负数区间。下面分析单精度浮点数,而双精度浮点数与其相比只是位数不同。以当精度类型为例,它被分配了4个字节,总共32位。 

从数学世界的科学计数法映射到计算机世界的浮点数时,数制从十进制改为二进制,还要考虑内存硬件设备的实现方式。在规格化表示上存在差异,称谓有所改变,指数称为“价码”,有效数字称为“尾数”,所以用于存储符号、价码、尾数的二进制位分别称为符号位、价码位、尾数位。

1 符号位

在最高二进制位上分配1位表示浮点数的符号,0表示正数,1表示负数。

2价码位

在符号位右侧分配8位用来存储指数,IEEE754标准规定价码位存储的是指数对应的移码,而不是指数的原码和补码。根据计算机组成原理中对移码的定义可知,移码是将一个真值在数轴上正向平移一个偏移量之后得到的,即[x]^{移}^{移}移 = x + 2^{n - 1}(n为x的二进制位数,含符号位)。移码的几何意义是把真值映射到一个正数域,其特点是可以直观地反映两个真值的大小,即移码大的真值也大。基于这个特点,对计算机来说用移码比较两个真值的大小非常简单,只要高位对齐后逐个比较即可,不用考虑符号的问题,这也是价码会采用移码表示的原因所在。

由于价码实际存储的指数的移码,所以指数与价码之间的换算关系就是指数与它的移码之间的换算关系。假设指数的真值为e,价码为E,则有E = e + (2^{n - 1} - 1),其中2^{n - 1} - 1是IEEE754标准规定的偏移量,n = 8是价码的二进制位数。

为什么偏移量值为2^{n - 1} - 1而不是2^{n - 1}呢? 因为8个二进制位能表示的指数的取值范围为[-128, 127], 现在将指数变成移码表示,即区间[-128, 127]正向平移到正数域,区间里的每个数都需要加上128,从而得到价码范围为[0, 255]。由于计算机规定价码全为0或全为1两种情况被当做特殊值处理(全0被认为是机器零, 全1被认为是无穷大),去除这两个特殊值,价码的取值范围变成了[1, 254]。如果偏移量不变仍为128的话,根据换算关系公式[x]阶 = x + 128 得到指数的范围变成[-127, 126], 指数最大只能取到126,显然会缩小浮点数能表示的取值范围。所以IEEE754标准规定单精度的价码偏移量为2^{n - 1} - 1(即127),这样能表示的指数范围为[-126, 127],指数最大值能取到127。

3尾数位

最右侧分配连续的23位用来存储有效数字,IEEE754标准规定尾数以原码表示。正指数和有效数字的最大值决定了32位存储空间能够表示浮点数的十进制最大值。指数最大值为2^{127} \approx 1.7 x 10^{38},而有效数字部分最大值是二进制的1.111···1(小数点后23个1),是一个无限接近于2的数字,所以得到最大的十进制数为2 x 1.7 x 10^{38}, 在加上一个符号位,最终得到32位浮点数最大值为3.4e+38。

0111 1111 0111 1111 1111 1111 1111 1111

  • 红色部分为符号位,值为0,表示正数。

  • 绿色部分为价码位即指数,值为{\color{DarkBlue}{\color{DarkBlue} }2^{254-127} = 2^{127} \approx 1.7 \times 10^{38} }

  • 黄色部分为尾数位即有效数字,值为1.11111111111111111111111

科学计数法进行规格化的目的是保证浮点数表示唯一性。如同十进制规格化的要求1≤ |a| <10,二进制数值规格化后的尾数形式为1.xyz, 满足1≤ |a| < 2。为了节约存储空间,将符合规格化尾数首个1省略,所以尾数表面上是23位,却表示了24位二进制数。

常用浮点数的规格化表示如表1 - 5所示。

表1-5 浮点数的规格化表示
数值 浮点数二进制表示 说明
-16 1100 0001 1000 0000 0000 0000 0000 0000

第一位为符号位,1表示负数。131-127 = 4,即2^{4} = 16,

尾数部分为1.0①

16.35 0100 0001 1000 0010 1100 1100  1100 1101

第一位为符号位,0表示正数。绿色部分同上,

尾数部分见说明

0.35 0011 1110 1011 0011 0011 0011 0011 0011 此例说明16.35和0.35的尾数部分是不同的
1.0 0011 1111 1000 0000 0000 0000 0000 0000 127-127=0即2^{0}=1,尾数部分为1.0
0.9 0011 1111 0110 0110 0110 0110 0110 0110 126-127=-1即0.5②

注释:①尾数部分的有效数字为1.000 0010 1100 1100  1100 1101,将其转换成十进制值为1.021875,然后乘以2^{4}得到                      16.35000038。由此可见,计算机实际存储的值可能和真值是不一样的。补充说明,二进制小数转化为十进制小                        数,小数点后一位是2^{-1},依次累加即可,如1.00000101 = 1+2^{-6}+2^{-8} = 1.01953125

           ②0.9不能用有限二进制位进行精确表示,所以1-0.9并不精确的等于0.1,实际结果是0.100000024。

1.2.3 加减运算

在数学中,进行两个小数的加减运算时,首先要将小数点对齐,然后同位数进行加减运算。对两个采用科学计数法表示的数做加减运算时,为了让小数点对齐就需要确保指数一样。当小数点对齐后,再将有效数字按照正常的数进行加减运算。

(1)零值检测。检查参加运算的两个数中是否存在为0的数(0在浮点数是一种规定,即价码位和尾数位全为0),因为浮点数运算过程比较复杂,如果其中一个数为0,可以直接得出结果。

(2)对阶操作。通过比较阶码的大小判断小数点位置是否对齐。当阶码不相等时表示当前两个浮点数的小数点位置没有对齐,则需要通过移动尾数改变价码的大小,使二者最终相等,这个过程便称为对阶。尾数向右移动1位,则阶码值加1,反之减1。在移动尾数时,部分二进制位会被移出,但向左移使高位被移出,对结果造成的误差更大。所以,IEEE754规定对阶的移动方向为向右移动,即选择阶码小的数进行操作。

(3)尾数求和。当对阶完成后,直接按位相加即可完成求和(如果是负数则需要先转换成补码在进行运算。)这个道理与十进制数加法相同,例如9.8 x 10^{38} 与 6.5 x 10^{37} 进行求和,将指数小的进行升阶,即6.5 x 10^{37} 变成0.65 x 10^{38},然后求和得到结果为10.45 x 10^{38}

(4)结果规格化。如果运算结果仍然满足规格化形式,则无须处理,否则需要通过尾数位的向左或向右移动调整达到规格化形式。尾数位向右移称为右规,反之称为左规。如上面计算结果为10.45 x 10^{38}, 右规操作后为1.045 x 10^{39}

(5)结果舍入。在对阶过程或右规时,尾数需要右移,最右端被移出的位会被丢弃,从而导致结果精度的损失。为了减少这种精度的损失,先将移出的这部分数据保存起来,称为保护位,等到规格化后再根据保护位进行舍入处理。

了解了浮点数的加减运算过程后可以发现,阶码在加减运算过程中是用来比较大小,从而决定是否需要进行对阶操作。所以,IEEE754标准正对这一特性,将阶码采用移码表示,目的就是利用移码的特点来简化两个数的比较操作。下面针对前面的例子从对阶、按位减法的角度分析为什么1.0-0.9结果为0.100000024,而不是理论值0.1。1.0-0.9等价于1.0+(-0.9)的二进制编码:

1.0 的二进制编码为  0011 1111 1000 0000 0000 0000 0000 0000

-0.9 的二进制编码为  1011 1111 0110 0110 0110 0110 0110 0110

从上面可以得出二者的符号、阶码、尾数三部分数据,如表1-6所示。

表1-6 符号、阶码与尾数
浮点数 符号 阶码 尾数(红色表示规格化后最高位) 尾数补码
1.0 0 127 1000 0000 0000 0000 0000 0000 1000 0000 0000 0000 0000 0000
-0.9 1 126 1110 0110 0110 0110 0110 0110 0001 1001 1001 1001 1001 1010

由于尾数位的最左端存在一个隐藏位,所以实际尾数二进制分别为:1000 0000 0000 0000 0000 0000 和 1110 0110 0110 0110 0110 0110,红色为隐藏位。下面运算是基于实际的尾数位进行的,具体过程如下:

(1)对阶。1.0的阶码为127, -0.9的阶码为126,比较阶码大小后需要右移的是-0.9尾数的补码,使其阶码变为127,同时高位需要补1,移动后的结果为1000 1100 1100 1100 1100 1101,最左的1是高位补进的。注意,绿色的数字仅仅是为了方便阅读,更加清晰观察到数字为的对齐或整体移动方向。

(2)尾数求和。因为尾数都转换成补码,所以可以直接按位相加,注意符号位也要参与运算,如图1-5所示。

 

                                                                                                 图1-5    尾数求和示意

其中最左端位符号位,计算结果为0,尾数位计算结果为0000 1100 1100 1100 1100 1101。

(3)规格化。上一步计算结果并不符合要求,位数的最高位必须是1,所以需要将结果向左移4位,同时阶码需要减4.移动后阶码等于123(二进制为1111011),尾数为1100 1100 1100 1100 1101 0000。在隐藏尾数的最高位,进而变为100 1100 1100 1100 1101 0000。

综上所述,得出运算后结果的符号位0、阶码为1111011、尾数为100 1100 1100 1100 1101 0000,三部分组合起来就是1.0-0.9的结果,对应的十进制值为0.100000024。

思考如下代码运行结果

float g = 1.0f - 0.9f;
float h = 0.9f - 0.8f
/** 第一种,判断浮点数是否相等的方式 */
        
if (g == h){
    System.out.println("true");
}else {
    System.out.println("false");
}

/** 第二种, 判断浮点数是否相等的方式 */
Float x = Float.valueOf(g);
Float y = Float.valueOf(h);
if (x.equals(y)){
    System.out.println("true");
}else {
    System.out.println("false");
}

/** 第三种, 判断浮点数相等的方式 */
Float m = new Float(g);
Float n = new Float(h);
if (m.equals(n)){
    System.out.println("true");
}else {
    System.out.println("false");
}      

相信以上代码的运行结果会让人大跌眼镜,输出结果为3个false!1.0f - 0.9f与0.9f - 0.8f 的结果理应都为0.1,但实际是不行等的。上面已经分析出1.0f - 0.9f = 1.00000024,那么0.9f - 0.8f 的结果为多少呢? 0.9f - 0.8f等价于0.9+(-0.8),首先分析0.9与-0.8的二进制编码:

0.9的二进制编码为0011 1111 0110 0110 0110 0110 0110 0110

-0.8的二进制编码为 1011 1111 0100 1100 1100 1100 1100 1101

从上可以得出二者的符号、阶码、尾数三部分数据,如表1-7所示。

表1-7 0.9与-0.8的符号、阶码与尾数
浮点数 符号 阶码 尾数(红色表示规格化后的最高位) 尾数补码
0.9 0 126 1110 0110 0110 0110 0110 0110  1110 0110 0110 0110 0110 0110
-0.8 1 126 1100 1100 1100 1100 1100 1101 0011 0011 0011 0011 001 0011

 由于尾数位的最左端存在一个隐藏位,所以实际尾数二进制分别为:1110 0110 0110 0110 0110 0110 和 1100 1100 1100 1100 1100 1101,红色为隐藏位。下面运算都是基于实际的尾数进行的,具体过程如下:

(1)对阶。0.9和-0.8的阶码都为126,不需要进行移阶运算。

(2)尾数求和。因为尾数都转化成补码,所以可以直接按位相加,注意符号位也要参与运算,如图1-6所示。

 其中最左端为符号位,计算结果为0,尾数位计算结果为0001 1001 1001 1001 1001 1001

(3)规格化。上一步计算结果并不符合要求,尾数的最高位必须是1,所以需要将结果向左移3位,同时阶码需要减3.位移后阶码等于123(二进制为 1111011),尾数为1100 1100 1100 1100 1100 1000。再隐藏尾数的最高位,进而变为100 1100 1100 1100 1100 1000。

综上所述,得到运算结果的符号位为0、阶码为1111011、尾数为100 1100 1100 1100 1100 1000,三部分组合起来就是0.9f - 0.8f的结果,对应的十进制数值为0.099999964。

因此在浮点数比较时正确的写法:

float g = 1.0f - 0.9f;
float h = 0.9f - 0.8f;

double diff = 1e-6;
if (Math.abs(g - h) < diff){
    System.out.println("true");
}else {
    System.out.println("false");
}

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
if (x.equals(y)){
    System.out.println("true");
}else {
    System.out.println("false");
}

1.2.4 浮点数的使用

在使用浮点数时推荐使用双精度,使用双精度由于表示区间的限制,计算结果会出现微小的误差,实例代码如下所示:

float ff = 0.9f;
double dd = 0.9d;
// 0.89999997615811421
System.out.println(ff/1.0);
// 0.9
System.out.println(dd/1.0);

在要求绝对精确表示的业务场景下,比如金融行业的货币表示,推荐使用整型存储其最小单位的值,展示时可以转换成货币的常用单位,比如人民币使用分存储,美元使用美分存储。在要求精确表示小数点n位的业务场景下,比如圆周率要求存储小数点后1000位数字,使用单精度和双精度浮点数类型保存是难以做到的,这时推荐采用数组保存小数部分的数据。在比较浮点数时,由于存在误差,往往会出现意料之外的结果,所以禁止通过判断两个浮点数是否相等来控制某些业务流程。在数据库中保存小数时,推荐是使用decimal类型,禁止使用float类型和double类型。因为这两种类型在存储的时候,存在精度损失的问题。

综上所述,在要求绝对精度表示的业务场景中,在小数保存、计算、转型过程中都需要谨慎对待。 


猜你喜欢

转载自blog.csdn.net/weixin_43222122/article/details/104301128