https://www.cnblogs.com/zuoxiaolong/p/computer12.html

前言

  上一章我们简单介绍了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;}}

  上面是测试程序,其实程序中看不出什么,就是一堆输出语句。如果各位猿友有兴趣,也可以简单看一下程序的实现。不过我们主要还是看结果,下面是程序结果。

  上面一共有8次舍入,前4次是正数,后4次是负数。可以看出对于正负数来讲,舍入后的位表示是一样的,只是最高位的符号位不同而已,因此这里LZ就不再分析下面4个负数的舍入方式了,我们主要来看前4次舍入。

  第1次和第2次对于末尾01和11的舍入,由于是中间值,因此全部采取的向偶数舍入的方式,保证最低位为0。第3次由于比中间值大,而数值又是正数,因此采用向上舍入的方式。第4次则比中间值小,数值也同样是正数,因此采用向下舍入的方式。

  由此可以看出,Java正是采用的我们所描述的方式进行舍入操作的,也就是总是企图朝最近的数值舍入。相对于其它语言,由于LZ主修Java,例子篇幅也比较长,因此这里就不写其他语言的例子了,有兴趣的猿友可以尝试写一下C/C++或者C#的例子来看一下,看是否是采用的同样的舍入方式。

浮点数运算

  在IEEE标准中,制定了关于浮点数的运算规则,就是我们将把两个浮点数运算后的精确结果的舍入值,作为我们最终的运算结果。正是因为有了这一个特殊点,就会造成浮点数当中,很多运算不满足我们平时熟知的一些运算特性。

  比如加法的结合律,也就是a + b + c = a + (b + c),这是很普通的加法运算的特性,但是浮点数是不满这一特性的,比如说下面这一段小程序。

    public static void main(String[] args){System.out.println(1f + 10000000000f - 10000000000f);System.out.println(1f + (10000000000f - 10000000000f));}

  这一段程序会依次输出0.0和1.0,正是因为舍入而造成的这一误差。在第一个输出语句中,计算1f+10000000000f时,会将1这个有效数值舍入掉,而导致最终结果为0.0。而在第二个输出语句中10000000000f-10000000000f将先得到结果0.0,因此最终的结果为1.0。

  相应的,浮点数运算对乘法也不满足结合律,也就是 a * b * c != a * (b * c),同时也不满足分配律,即 a * (b + c) != a * b + a * c。

  浮点数失去了很多运算方面的特性,因此也导致很多优化手段无法进行,比如我们试图优化下面这样一段程序。

        /*   优化前       */float x = a + b + c;float y = b + c + d;/*   优化后       */float t = b + c;float x = a + t;float y = t + d;

  对于优化前的代码来讲,进行了4次浮点运算,而优化后则是3次。然而这种优化是编译器无法进行的,因为可能会引入误差,比如就像前面的小例子中的结果0和1一样。编译器在此时一般是不敢进行优化的,试想一下,如果是银行系统的汇款或者收款等功能,如果编译器进行优化的话,很可能一不小心就把别人的钱给优化掉了。

文章小结

  2.X系列主要讲解了二进制的位表示方式、无符号以及补码编码以及二进制整数和浮点数的表示方式和运算。这一章是2.X的最后一章,下一章我们将进入汇编语言3.X的世界,那里我们可以看到程序是如何使用寄存器和存储器的、如何表示C语言中的指针、汇编语言如何实现程序的流程控制等等一系列内容。相对来讲,3.X的内容会比2.X的内容有意思很多,因此希望各位猿友不要错过。

版权声明


作者:zuoxiaolong(左潇龙)

出处:博客园左潇龙的技术博客--http://www.cnblogs.com/zuoxiaolong

您的支持是对博主最大的鼓励,感谢您的认真阅读。

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

深入理解计算机系统(2.8)---浮点数的舍入,Java中的舍入例子以及浮点数运算(重要)相关推荐

  1. java中小数的处理:高精度运算用bigDecimal类,精度保留方法,即舍入方式的指定

    java中小数的处理:高精度运算用bigDecimal类,精度保留方法,即舍入方式的指定 2016年05月11日 11:20:08 阅读数:6336 一. 计算机的小数计算一定范围内精确,超过范围只能 ...

  2. 《深入理解计算机系统》读书随笔-位操作

    最近开始读<深入理解计算机系统>这本书.对于书中提到的从程序员的角度解读计算机系统这一说法非常感兴趣,所以决定好好读一读.从开始接触计算机编程就是站在一个高级语言的层次,虽然对编译原理,操 ...

  3. Java中的浮点数四舍五入到小数点后2位的一些实用方法

    前言 四舍五入到2或3个小数位是我们Java程序员日常开发中肯定会遇到.幸运的是,Java API提供了几种在Java中舍入数字的方法 我们可以使用Math.round(),BigDecimal或De ...

  4. 浮点数0.7在Java中是无法精确存储的,却为何能精确输出0.7

    2019独角兽企业重金招聘Python工程师标准>>> 这是在其他地方看到的一个提问,提问原文链接:https://www.oschina.net/question/2346828_ ...

  5. java中,剩下的这两个内部类不太好理解!

    点击上方蓝色关注我们! 大家好,我是雄雄,今天我们接着昨天的分享,将剩余的两个内部类(方法内部类和匿名内部类)结束掉,这两个内部类都不太好理解. 昨天的推文:java中常见的几种内部类,你会几个?(未 ...

  6. 深入理解计算机系统(2.7)------浮点数舍入以及运算

    上一篇博客我们讲解了二进制小数如何表示以及IEEE浮点标准.而且我们也提到过因为这种表示方法限制了浮点数的范围和精度,浮点数只能近似的表示一个数. 比如 数字1/5,我们能用十进制小数 0.2 准确的 ...

  7. 深入理解计算机系统之浮点数

    一.什么是浮点数 了解浮点数这个概念前,先要了解什么是定点数,定点数的概念为: 定点表示即约定机器数中的小数点位置是固定不变的,小数点不再使用"."表示,而是约定它的位置,即在固定 ...

  8. 《深入理解计算机系统-程序结构》读书笔记

    1.计算机系统漫游 计算机系统是由硬件和系统软件组成的,他们共同工作来运行应用程序.在<深入理解计算机系统>一书中将会学到很多实践的技巧.例如:了解编译器是如何实现过程调用的.避免缓冲区溢 ...

  9. 《深入理解计算机系统》读书笔记

    <深入理解计算机系统>读书笔记 注:<深入理解计算机系统>是我们<系统级编程>课程的参考书.这里主要记载的是在看<深入理解计算机系统>这本书的过程中,遇 ...

最新文章

  1. 服务的实例已在运行中_亚马逊首次把macOS引入云服务
  2. 参加金蝶OperaMasks-WebFramework成都推广活动后的感想
  3. .net core ——利用 roslyn 编译C#代码
  4. 小县城里的体制内剩女:一个日益庞大却被悬置的群体?
  5. Eclipse 答疑:为什么在 Eclipse 中,运行本程序却是另外一个程序的结果?
  6. 总结:Sharepoint2010 Client Object Model -- ECMAScript Client
  7. 推荐分享一个自定义绑定控件(附源码)
  8. Conditional GET Request(缓存协商)
  9. Alex 的 Hadoop 菜鸟教程: 第14课 Sqoop1 从Hbase导出mysql
  10. flash将文本呈现为html,flash中的静态文本、动态文本、输入文本
  11. 实验七matlab数值计算,数学应用软件实验报告---MATLAB的数值计算
  12. python pyhook监听扫码_Python2.7:使用Pyhook模块监听鼠标键盘事件-获取坐标实例
  13. python学习笔记--缓解眼睛疲劳的小工具
  14. 基于李雅普诺夫函数的跟踪控制(一)
  15. Java一般要学多久?
  16. excel odc连接文件的创建和使用
  17. 【FFT-类字符串匹配】LOJ6388 [THUPC2018]赛艇 / Citing
  18. 增强现实中的光学透射式头盔显示器的标定初步
  19. Java快递驿站项目
  20. 【计算机网络】网络层——IPv6/IP组播/移动IP

热门文章

  1. 三重积分的球面坐标系的体积元素表示
  2. Cg语言学习笔记(1)
  3. diff函数求函数的导函数、偏导函数,及在某一点的导数、偏导数 --python
  4. 易安卓读取HTML,易安卓(E4A)怎么保存设置?
  5. 云从科技资深算法研究员:详解跨镜追踪(ReID)技术实现及难点 | 公开课笔记
  6. 图像直方图均衡化和空间滤波
  7. 【MyBatis-Plus】CRUD 操作
  8. awk命令详解(二)
  9. Beyond Compare 4.4.2.26348发布 附下载链接
  10. 如何将WIN10自带浏览器Microsoft Edge中的书签导出