什么是浮点数
浮点数是在计算机中用以近似的表示任意的某个实数。具体的说,这个实数由一个整数或定点数(即尾数),乘以某个基数(计算机中通常为2)的整数次幂得到的,这种表示方法类似于基数为10的科学记数法。
为什么要用浮点数
在看到本文之前,想必大家对整型数据类型已经有了一定的了解。然而在实际生活中,又或者程序编写中,不可避免的需要使用到小数。那么,在计算机小数究竟是怎样存储和运算的呢?
科班出身的同学对这个问题应该有所了解。实际上,运算器本身并不能处理小数,计算机也无法准确的存储和表示小数。这里以一个16位的变量为例空间,请大家思考一下,怎么用这样大小的空间去表示一个小数呢?
最简单的想法,就是模仿现实生活中小数的表示形式,将小数点固定在某个位置,小数点前表示整数,小数点后表示小数。这也即是数的定标。
通过将小数点设定在16位数的不同的位置,就可以表示不同大小和精度的小数。
数的定标有Q表示法和S表示法两种。下面给出16位空间的不同Q/S表示的范围。
Q表示 | S表示 | 数据范围 |
Q15 | S0.15 | -1<= x <=0.9999695 |
Q14 | S1.14 | -2<= x <=1.9999390 |
Q13 | S2.13 | -4<= x <=3.9998779 |
Q12 | S3.12 | -8<= x <=7.9997559 |
Q11 | S4.11 | -16<= x <=75.9995117 |
Q10 | S5.10 | -32<= x <=31.9990234 |
Q9 | S6.9 | -64<= x <=63.9980469 |
Q8 | S7.8 | -128<= x <=127.9960938 |
Q7 | S8.7 | -256<= x <=255.9921875 |
Q6 | S9.6 | -512<= x <=511.9804375 |
Q5 | S10.5 | -1024<= x <=1023.96875 |
Q4 | S11.4 | -2048<= x <=2047.9375 |
Q3 | S12.3 | -4096<= x <=4095.875 |
Q2 | S13.2 | -8192<= x <=8191.75 |
Q1 | S14.1 | -16384<= x <=16383.5 |
Q0 | S15.0 | -32768<= x <=32767 |
从上表我们不难得知,对于定点数实现小数来说,数值范围和精度永远是矛盾的。一个变量若是想要能够表示比较大的数值范围,必须以牺牲精度为代价;而若是想提高精度,则数的表示范围就必须相应的减小。
正因为定点数表示小数所存在的缺陷,所以我们才会使用浮点数来表示小数。下面我们正式开始介绍浮点数的底层原理。
正规化
对于将某个实数表示为计算机浮点数,首先要将其正规化,也就是表示形如:
±1.bbbbb...×2^p
其中b是0或1,而p表示二进制数的指数位。第一位符号位也用0、1代表正负。接着将指数p加上移码表示为N位的二进制数。最后的M位用来存放1.bbbb...的部分,由于正规化表示时,最左边的部分总是1,所以实际上只需要M-1位来表示尾数即可。
移码
以上描述中有一个词:移码(exponential bias)。因为指数p有正有负,那么就需要拿出一位来指示符号,显然这样会造成不必要的浪费。给指数加上移码,就能保证结果总是一个非负数,也可以将指数部分的N位都利用起来。对于有N个指数位的浮点数,其移码为:
2^(N-1)-1
这里我们利用该公式,先将下文中将要提到的IEEE 754标准中的三种精度所对应的移码表示出来
精度 | 阶码 | 移码 | 二进制表示 |
float | 8 | 127 | 0111 1111 |
double | 11 | 1023 | 011 1111 1111 |
longdouble | 15 | 16383 | 011 1111 1111 1111 |
以双精度浮点数double为例。双精度的指数位有11位,可以表示0~2047这个范围,那么,减去移码1023以后,可以指示的指数是-1023~1024,但由于-1023和1024另有他用(我们会在后文中详细叙述),所以双精度浮点数实际能表示的范围是-1022~1023。
IEEE 754标准
实际上,Java的两种浮点类型(float、double)所遵循的IEEE 754标准(IEEE浮点数算术标准)也是运用了以上的思想。
IEEE 754标准定义了32位和64位两种浮点数(其实还有一种80位的长双精度)。用科学计数法,以底数为2的小数来表示浮点数。
二进制浮点数是以符号数值表示法格式存储
对于32位浮点数float:第1位是数符S(表示底数的符号),2~9位为阶码E,最后23位为尾数M。
同理的,对于64位浮点数double:第1位是数符S,2~12位为阶码E,用最后52位为尾数M。
对于80位浮点数longdouble(长双精度):第1位是数符S,2~16位为阶码E,用最后64位为尾数M。
V = (-1)^S * (1+M) * 2^(E-127) (单精度)
V = (-1)^S * (1+M) * 2^(E-1023) (双精度)
机器ε(machine epsilon)
机器ε表示1与大于1的最小浮点数之差。不同精度定义的机器ε不同。以双精度为例,
双精度表示1是
1.000......0000(52个0) × 2^0
而比1大的最小的双精度是(其实还能表示更小的范围,后文中会提到,但并不影响这里的机器ε)
1.000......0001 × 2^0
也即
2^-52 ≈ 2.220446049250313e-16。所以它就是双精度浮点数的机器ε。
在舍入中,相对舍入误差不能大于机器ε的一半。
对于双精度浮点数来说,这个值为0.00000005960464477539。
所以在Java中double类型中连续8个0.1相乘,就会出现表示不精确的情况。
非正规化:0的表示
从正规化的定义中可知,无论如何浮点数都满足左边是1。但这就带来了一个严重的问题:0没有办法被表示。为此,可以使用非正规化的表示方法,即让最左边默认为0,再另尾数也全部为0,就可以表示0了。
那么什么情况下是非正规化、什么情况下又是非正规化呢?
这里我们通过指数来反映,前文中说过-1023和1024(实际上是0和2047)另作他用,实际上应用就在这个地方了:若指数部分为0时,尾数部分就不是1.bbbb...而是0.bbbb...。
进一步的说,对于非正规化,可以看成在正规化中,小数点向左移了一位。如:
1.bbbb...×2^-1023 = 0.1bbbb....×2^-1022
当然小数点后第一位不一定为1
综上,非正规化可以表示为
±0.b1b2b3....b52 × 2^-1022
也正因为非正规化后最左边不是1而是0,所以能表示更小的数。故双精度浮点数下,使用非正规化可以表示的最小的正数是
0.0....01 × 2^-1022
也即
2^-52 * 2^-1022 = 2^-1074
综上,得到三种精度的浮点数的取值范围
精度 | 最小正数 | 最大正数 | 最大负数 | 最小负数 |
float | 2^-149 | 2^128 | -2^-149 | -2^128 |
double | 2^-1074 | 2^1024 | -2^-1074 | -2^1024 |
longdouble | 2^-16446 | 2^16384 | -2^-16446 | -2^16384 |
需要注意的是这个最小数和前文提到的机器ε是有区别的。比机器机器ε还小的数是可以通过非正规化表示出来的,但当它们与其他浮点数一起进行运算时,因为要转换成同一种格式(正规化形式)进行计算,从而可能会因为溢出而被舍弃。
最终造成的结果就是,尽管这些更小的数能够被表示,但是对于运算结果却没有影响。
无穷大与NaN
上面说到,在双精度浮点数中,指数为0表示非正规化,那么指数为2047(111 1111 1111b)时,就表示无穷大和NaN。
具体区别表现在,当指数是2047时,尾数全为0,就表示无穷大;当尾数不全为0就表示NaN(Not a Number),故NaN并不是一个数而是一族。
浮点数加法
浮点数进行计算时,要先将两个操作数的小数点对齐,也即将指数对齐后,相加再转为浮点数存储。这里最重要的一点是,尽管浮点数有位数限制,但是加法会在精度更高的寄存器中进行,以双精度浮点数为例,寄存器能够运算出比52位还要多的位数(临时数一般为longdouble精度——1符号位,15位阶码,64位尾数),但是在转回浮点数进行存储的过程中,多余的位数会被舍弃,造成两者相加的结果不严格的等于算术结果。
如: 1 + 2^-53
= 1 * 2^0 + 1* 2^-53
= 1 * 2^0 + 0.00...001(小数点后52个0) * 2^0
= 1.00..001(小数点后52个0) * 2^0
= 1.00..00(小数点后52个0) * 2^0(转储为双精度浮点数时,只能保存52位二进制尾数)
= 1
对于数学运算来说。任何一个数加另一个非0数,都大于它本身。
这里就可以看出进行浮点数运算的不精确。
εmath = 2^-52并不代表在IEEE模型中可以忽略比εmath 更小的数,只要它们在模型中能被正确标识出来,假定它们没有与单位大小的数相加减,那么用这样大的数进行计算就如同精确的一样。
浮点数舍入规则
以52位尾数的双精度浮点数为例,舍入时需要重点参考第53位。
若第53位为1,而其后的位数都是0,此时就要使第52位为0;若第52位为0则不用再进行其他操作,若第52位为1,则第53位就要向52位进一位。
若第53位为1,但其后的位数不全为0,则第53为就要向第52位进一位。
若不是以上两种情况,也即53位为0,那么就直接舍弃不进位,称为下舍入。
浮点数舍入规则也就证明了为何在上文中提到的浮点数舍入中,相对舍入误差不能大于机器ε的一半。
对于java来说,一般float类型小数点后保留7位,而double类型小数点后保留15位。
这个原因也是因为尾数的数据宽度限制
对于float型来说,因为2^23 = 8388608
同时最左一位默认省略了,故实际能表示2^24 = 16777216个数,最多能表示8位,但绝对精确的只能表示7位。
而对于double型来说,2^52 = 4503599627370496,共16位。加上省略的一位,能表示2^53 = 9007199254740992。故double型最多能表示16位,而绝对精确的只能表示15位。
浮点数的存储为什么是不精确的
从上文中,我们不难了解,浮点数的运算会因为数据范围问题而产生溢出,进而被舍弃,从而造成计算结果不精确。
但我们还需要知道的是,浮点数不仅仅在计算时不精确,在存储时也并不精确。想要了解原因,我们先来简单了解一下关于浮点数十进制转化为二进制的规则:
整数部分除二取余,倒序输出;小数部分乘二取整,正序输出。
举个栗子:
12.125(10)
根据以上规则
其整数部分为1100
小数部分为001
得到其二进制小数1100.001
这样看并没有不精确的情况,但是我们换一个数来进行计算
再举个栗子:
12.6
其整数部分为1100
小数部分为100110011001...
其小数部分的计算过程为1.2 -> 0.4 -> 0.8 -> 1.6 -> 1.2 -> 0.4 -> 0.8 ...
这是无限循环的,可以预见,当其数据宽度超过double型尾数所能容纳的52位时,新的问题出现了——由于溢出,多余的位数无法被表示。12.6在计算机中的存储变的不精确了。
然而这样的情况并不少见。所以大部分时候计算机并不能精确的存储一个浮点数。
总结
总的来说,浮点数在计算机中是以一种类似科学计数法的方式(IEEE 754)存储和运算的。
浮点数的计算是不精确的——舍入误差和表示宽度溢出问题的存在
浮点数的存储也是不精确的——二进制存储小数的特性,超过精度后溢出的部分将不会被存储。于是出现误差。