计算机系统原理(十二) 浮点数的舍入、Java中舍入例子及浮点数运算2014-08-22
前言
上一章我们简单介绍了IEEE浮点标准,本次我们主要讲解一下浮点运算舍入的问题,以及简单的介绍浮点数的运算。之前我们已经提到过,有很多小数是二进制浮点数无法准确表示的,因此就难免会遇到舍入的问题。这一点其实在我们平时的计算当中会经常出现,就比如之前我们提到过的0.3,它就是无法用浮点小数准确表示的。为此LZ专门写了一个小程序,使用Java语言打印出了0.3的二进制表示,是这样的一个数字,0 01111101 00110011001100110011010。我们来简单算一下,这个数值大约是多少。它的阶码在偏置之后的值为-2,它的尾数位在加1之后为1 + 1/8 + 1/16 + 1/128 + 1/256 = 1.19921875。后面还有有效位,不过我们只大概计算一下,就不算那么精确了,最终算出来的值为0.2998046875。(LZ用计算器算的,0.0)可以看出,这个值离0.3已经非常接近了,而且我们还省略了一小部分有效小数位,但是不管怎么说,二进制无法像十进制小数一样,准确的表示0.3这个数值。因此舍入这一部分是浮点数无法逃脱的内容。
浮点数舍入
在我们平时日常使用的十进制当中,我们一般对一个无理数或者有位数限制的有理数进行舍入时,大部分时候会采取四舍五入的方式,这算是一种比较符合我们期望的舍入方式。不过针对浮点数来说,我们的舍入方式会更丰富一些。一共有四种方式,分别是向偶数舍入、向零舍入、向上舍入以及向下舍入。这四种舍入方式都不难理解,其中向偶数舍入就是向最靠近的偶数舍入,比如将1.5舍入为2,将0.1舍入为0。而向零舍入则是向靠近零的值舍入,比如将1.5舍入为1,将0.1舍入为0。对于向上舍入来说,则是往大了(也就是向正无穷大)舍入的意思,比如将1.5舍入为2,将-1.5舍入为-1。而向下舍入则与向上舍入相反,是向较小的值(也就是向负无穷大)舍入的意思。这里需要提一下的是,除了向偶数舍入以外,其它三种方式都会有明确的边界。这里的含义是指这三种方式舍入后的值x"与舍入之前的值x会有一个明确的大小关系,比如对于向上舍入来说,则一定有x <= x"。对于向零舍入来说,则一定有|x| >= |x"|。对于向偶数舍入来讲,它最大的作用是在统计时使用。向偶数舍入可以让我们在统计时,将舍入产生的误差平均,从而尽可能的抵消。而其它三种方式在这方面都是有一定缺陷的,向上和向下舍入很明显,会造成值的偏大或偏小。而对于向零舍入来讲,如果全是正数的时候则会造成结果偏小,全是负数的时候则会造成结果偏大。通常情况下我们采取的舍入规则是在原来的值是舍入值的中间值时,采取向偶数舍入,在二进制中,偶数我们认为是末尾为0的数。而倘若不是这种情况的话,则一般会有选择性的使用向上和向下舍入,但总是会向最接近的值舍入。其实这正是IEEE采取的默认的舍入方式,因为这种舍入方式总是企图向最近的值的舍入。比如对于10.10011这个值来讲,当舍入到个位数时,会采取向上舍入,因此此时的值为11。当舍入到小数点后1位时,会采取向下舍入,因此此时的值为10.1。当舍入到小数点后4位时,由于此时为10.10011舍入值的中间值,因此采用向偶数舍入,此时舍入后的值为10.1010。
Java当中的浮点数舍入
之前我们讲解了一堆舍入的方式,最终我们给出一个结论,就是IEEE标准默认的舍入方式,是企图向最近的值舍入(Round to the Nearest Value)。上面我们已经详细的解释了IEEE标准中默认的舍入方式(黑色加粗的那部分解释),但是估计还是会有不少猿友比较迷糊,书中也没有给出具体的例子,因此这里LZ以Java语言为例,我们直接写程序来看一下,看看Java当中的舍入方式是否是按照我们所说的进行的。在各位看这个测试程序之前,LZ需要再给各位再解释一下中间值的概念。中间值就是指的,比如1.1(二进制)这个数字,假设要舍入到个位,那么它就是一个中间值,因为它处于1(二进制)和10(二进制)的中间,在这个时候将会采用向偶数舍入的方式。下面便是LZ写的测试程序,其中那些具体的浮点数值是使用二进制小数的算法计算出来的,各位猿友不必在意,如果你不嫌麻烦,也可以自己手算一下。我们主要看的是最终的舍入情况。
public class Main{public static void main(String[] args){System.out.println("舍入前: 10.10011111111111111111101");System.out.print("舍入后:");printFloatBinaryString(2.62499964237213134765625f);System.out.println();System.out.println("舍入前: 10.10011111111111111111111");System.out.print("舍入后:");printFloatBinaryString(2.62499988079071044921875f);System.out.println();System.out.println("舍入前: 10.10011111111111111111101011");System.out.print("舍入后:");printFloatBinaryString(2.62499968707561492919921875f);System.out.println();System.out.println("舍入前: 10.10011111111111111111100011");System.out.print("舍入后:");printFloatBinaryString(2.62499956786632537841796875f);System.out.println();System.out.println("舍入前:-10.10011111111111111111101");System.out.print("舍入后:");printFloatBinaryString(-2.62499964237213134765625f);System.out.println();System.out.println("舍入前:-10.10011111111111111111111");System.out.print("舍入后:");printFloatBinaryString(-2.62499988079071044921875f);System.out.println();System.out.println("舍入前:-10.10011111111111111111101011");System.out.print("舍入后:");printFloatBinaryString(-2.62499968707561492919921875f);System.out.println();System.out.println("舍入前:-10.10011111111111111111100011");System.out.print("舍入后:");printFloatBinaryString(-2.62499956786632537841796875f);System.out.println();}public static void printFloatBinaryString(Float f){char[] binaryChars = getBinaryChars(f);for (int i = 0; i < binaryChars.length; i++) {System.out.print(binaryChars[i]);if (i == 0 || i == 8) {System.out.print(" ");}}System.out.println();}public static char[] getBinaryChars(Float f){char[] result = new char[32];char[] binaryChars = Integer.toBinaryString(Float.floatToIntBits(f)).toCharArray();if (binaryChars.length < result.length) {System.arraycopy(binaryChars, 0, result, result.length - binaryChars.length, binaryChars.length);for (int i = 0; i < result.length - binaryChars.length; i++) {result[i] = "0";}}else {result = binaryChars;}return result;}}