从内存角度分析浮点数大小比较方法

        在我们日常开发中我们经常会遇到比较浮点数大小的问题,一般来说我们不能直接像整型那样比较(形如1==2),因为浮点型在内存中的存储方式是不同于整型,因为浮点数在内存中存储的是一个近似数值而不是精确数值,下边我们将从内存的角度分析为何浮点数存储时会有误差,以及浮点数常用的比较方法。


一、二进制表示小数为何会造成误差


        理解浮点数的第一步是考虑含有小数的二进制数字,首先我们从熟悉的十进制开始分析,一般来说十进制表示法用下边形式来进行表示:
        $$ d_{m}d_{m-1}d_{m-2}\cdots d_{0}.d_{-1}d{-2}\cdots d_{-n} $$   (形如9876.54321

其中每一个十进制数d_{i}的取值范围为0-9,将其用数学累加式表示出来如下:

        d= \sum_{i=n}^{m} 10^ixd_i

同理我们可以通过这种方法用二进制来表示一个小数:

                   b= \sum_{i=n}^{m} 2^ixb_i

例如101.11_2表示5\frac{3}{4}通过二进制表示出来就是1*2^2+0*2^1+1*2^0+1*2^{-1}+1*2^{-2}

由于计算机空间不是无线的,那么计算机在分配给数据空间时其编码长度也是有限的,因此我要考虑在编码长度有限的条件下如何进行数据表示。以十进制为例它不能保存像\tfrac{1}{3} \tfrac{5}{7}这样不能除尽的分数,同理二进制在表示也只能精确表示那些可以被除尽的数,即能够写成b= \sum_{i=n}^{m} 2^ixb_i的数,对于那些除不尽的数,我们只能近似表示而永远无法准确表示,这也就是浮点数在表示某些小数会造成误差的原因。

 

二、浮点数在内存中实际表示方法

          根据IEEE标准浮点数应该用V=(-1)^{s}\times M \times 2^E表示,其中:

          符号(s):当s=0时V为整数,当s=1时,V为负数。

          尾数(M):又称为基数,是一个二进制小数

           E(阶码):E的作用是对尾数加权

        

           将浮点数的位表示划分为三个字段,分别对这些字段编码可得:

           符号位s:直接编码为一位的s。

            阶码(E):表示为exp=e_{k-1}\cdots e_1e_0编码阶码E。

            尾数(M):表示为frac=f_{n-1}\cdots f_1f_0
    即在内存中浮点表示方法如下:

 

        给定位表示,根据exp的值,被编码的值可以分为三种不同情况(最后一种有两种变体),下图说明了对单精度格式要求的三种情况。

          情况一: 规范化的值:

           这是最常见的情况,当exp表示位(即e_{k-1}\cdots e_1e_0)不全为0且不全为1时,此时就属于规范化的情况。在这种情况中,阶码值(exp)被解释为以偏执形式表示的有符号数,即阶码的值E=exp-Bias,其中exp=e_{k-1}\cdots e_1e_0,Bias=2^{k-1}-1,此时,产生了指数的取值范围-(2^{k-1}-2)\sim 2^{k-1}-1,即对于单精度浮点型来说其取值范围为-126\sim +127  (k=8),对于双精度浮点型来说就是-1022\sim 1023  (k=11)。

      [注]引入偏执数Bias的原因:

只有通过减少偏执数Bias,才可以将原来阶码表示范围0\sim 2^{k}-1移动到-(2^{k-1}-2)\sim 2^{k-1}-1,从而使得阶码既可以表示正数也可以表示负数。

小数字段frac被解释为表示小数f,其中0\leqslant f< 1,用二进制表示为0.f_{n-1}\cdots f_1f_0,也就是说二进制小数点在最高位有效位的左边。尾数(M)定义为M=1+f。这种方式叫做隐含以1开头的表达方式(implied leading 1),从而我们可以把M看做是一个二进制表示为1.f_{n-1}\cdots f_1f_0二进制表达式。通过这种以1开头的表示方式我们将M的范围变置1\leqslant M<2(假设没有溢出),这种表示方法是一种额外获得额外精度位的技巧。即既然第一位总是等于1,那么我们就没有必要显示的表示出来。

        情况二:非规范化的值

        当阶码域全为0时,所表示的数就是非规范化形式。在这种情况下,阶码值被解释为E=1-Bias,而尾数的值被解释为M=f,此时M范围为0\leqslant M<1,即在此种情况下小数字段不隐含以1开头。

[注]为何阶码值被表示为1-bias而不是规范形式0-bias ?为何尾数被表示为M=f,而不是规范形式下的1+M?

改变阶码值表示形式主要是为了提供一种从规范化值平滑过渡到非规范化值的形式。因为非规范化主要有两个用途:首先,它提供了一种表示0的方法,因为使用规范化数我们必须使得M\geqslant 1,此时根据公式V=(-1)^{s}\times M \times 2^E,无论如何我们也无法表示0。而通过非规范化形式我们可以将符号位s,阶码值,尾数都置为0来表示0,即对于单精度浮点型我们用

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

来表示0。此时可能会有疑问,如果我们只将符号位置为1其他位都置为0不就表示为-0了,此时就涉及到计算机在内存中存储值的问题了,因为计算机在内存中存数据时存的都是其补码因此,对于

1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
其表示是-1而不是0。

非规范化数的另一个功能是表示那些非常接近0.0的数,这些接近于0的数有一些特性我们称之为逐渐溢出(gradual underflow),其中这些数分布均匀的接近0.0。

       情况三:特殊值

       最后一种情况指的是阶码值全为1,此时,当小数域全为零时,得到的值我们表示为无穷,其中s=0时表示为正无穷,s=1时表示为负无穷。当两个非常大的数相乘或者某个数除以0时,此时出现溢出现象,我们可以通过无穷来进行表示。当小数域不为零时,其表示NaN,即not a number。因为一些数的运算可能既不是实数也不是无穷,此时可以通过NaN来进行表示。

       数字表示的例子

       下图展示了假定的8位浮点格式的示例,其中有k=4的阶码位和n=3的小数位,偏执量Bias=2^{4-1}-1=7。图中针对不同的情况都举出了例子。

​三、浮点数的大小比较方法

正如上边所说的浮点数在内存中表示方法限制了浮点数的范围和精度,在内存中只能近似的表示一个实小数,即内存中存储的数与实际上要表示的数之间存在误差,因此针对浮点数的比较我们不能简单的用> < \geqslant \leqslant ==来进行比较。例如下边的例子

public static void main(String[] args) {
        //注意float存储的只是一个近似值
        float d1=423432423f;
        float d2=d1+0.0001f;
        System.out.println(d1==d2);
}

 

其运行结果为

 

因此浮点数在进行比较时,应该使用BigDecimal类来进行比较。即

public static void main(String[] args) {
        final float DELTA=0.000001f;
        //注意float存储的只是一个近似值
        float d1=423432423f;
        float d2=d1+0.01f;
        BigDecimal b1=BigDecimal.valueOf(d1);
        BigDecimal b2=BigDecimal.valueOf(d2);
        System.out.println(b1==b2);
}

 

其运行结果为:

或者我们也可以通过定义一个精度来进行比较,如果两个数的差值绝对值小于定义的精度我们便可以认为他们是相等的,否则我们认为这两个数是不相等的。

public static void main(String[] args) {
        final double DELTA=0.000001;
        //注意float存储的只是一个近似值
        double d1=423432423;
        double d2=d1+0.01f;
        System.out.println(Math.abs(d1-d2)<DELTA);
}

 

结果为

posted @ 2019-03-31 20:41  vcjmhg  阅读(681)  评论(0编辑  收藏  举报