深入理解Java浮点数机制【详析】

编程入门 行业动态 更新时间:2024-10-10 18:24:53

深入理解Java浮点数<a href=https://www.elefans.com/category/jswz/34/1771042.html style=机制【详析】"/>

深入理解Java浮点数机制【详析】

什么是浮点数

浮点数是在计算机中用以近似的表示任意的某个实数。具体的说,这个实数由一个整数或定点数(即尾数),乘以某个基数(计算机中通常为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)存储和运算的。

浮点数的计算是不精确的——舍入误差和表示宽度溢出问题的存在

浮点数的存储也是不精确的——二进制存储小数的特性,超过精度后溢出的部分将不会被存储。于是出现误差。

更多推荐

深入理解Java浮点数机制【详析】

本文发布于:2024-02-06 13:41:36,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1749385.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:机制   浮点数   Java   详析

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!