光棍节加长版

转:http://justjavac.com/codepuzzle/2012/11/11/codepuzzle-float-who-stole-your-accuracy.html

代码之谜(五)- 浮点数(谁偷了你的精度?)

如果我告诉你,中关村配置最高的电子计算机的计算精度还不如一个便利店卖的手持计算器,你一定会反驳我:「今天写博客之前又忘记吃药了吧」。

你可以用最主流的编程语言计算 0.2 + 0.4,如果你使用的是 Chrome、FireFox、IE 8+,可以按 F12 键,然后找到 「控制台」,输入上面的 表达式 0.2 + 0.4,回车。

然后再用最简陋的计算器(如果你没有手持计算器没关系,手机、电脑都自带一个计算器,打开“运行”,输入 calc,回车) 再计算一下刚才的 算式 0.2 + 0.4。

怎么样?同意我的观点了吧! 再简陋的计算器也比超级计算器的精度高,关键不在于它的频率和内存,而在于它是如何设计、如何表示、如何计算的。

不能表示 VS 不能精确表示

在上一章『浮点数(从惊讶到思考)』中我们讲到用浮点数表示 数 时出现的问题——很多数都 不能表示。(注意 浮点数表示的是数,而不仅仅是小数。)

如果你数学比较好,或者你确信你身体健康,没有心脏病、高血压,没有受过重大精神创伤,那我告诉你, 在浮点数的表示范围内,有多于 99.999...% 的数在计算机中是 不能表示 的。真的是太令人吃惊,也太令人遗憾了。真相总是很残忍。

请注意我使用的措辞,区别开 不能表示 和 不能精确表示。

下面我从数量级分析一下,32bit 浮点数的表示范围是 10 的 38 次方,而表示个数呢,是 10 的 10 次方。能够被表示的数只有 1/100000000.... (大概有30个零),这个数多大呢?还记得那个国际象棋和麦子的故事吗?

为了让你了解 指数的威力,我再举个例子:

有一张很大很大的纸,对折 38 次,会有多高呢?一米?一百米?比珠峰还高?再次考验你心脏承受能力的时刻到了:它不仅仅比珠峰高,其实它已经快到达月球了。

回到原来的话题,还有更残忍的真相。在剩下的可以表示的不到 0.000...1% 的数中,又有多少不能精确表示呢?这就是我写这篇博客的目的。

上一章中我还给出了一种用定点数精确表示小数的方法。事实上,手持计算器、java 中的 BigDecimal、C# 中的货币类型、MySQL 中的 NUMERIC 类型就是这么干的。你还记得在数据库中添加字段时的 SQL 语句是如何写的吗?现在明白为什么我说 再简陋的计算器也比超级计算器的精度高 了吧。

这篇博客我将为大家讲解为什么很多数 不能精确表示,本篇可能比较烧脑子,我会尽量用最通俗的语言,最贴近现实的例子来讲解,不在乎篇幅有多长,关键是要给大家讲明白。下一篇,你将了解到浮点数如何工作,以及为什么很多数 不能表示。

热身 —— 问:要把小数装入计算机,总共分几步?你猜对了,3 步。第一步:转换成二进制

第二步:用二进制科学计算法表示

第三步:表示成 IEEE 754 形式

在上面的第一步和第三步都有可能 丢失精度。

十进制 VS 二进制

下面我们讨论如何把十进制小数转换成二进制小数(什么?你不会?请自觉去面壁)。

考虑我们将 1/7(七分之一) 写成小数的时候是如何做的?

用 1 除以 7,得到的商就是小数部分,剩下的余数我们继续除以 7,一直除到什么时候结束呢?有两种情况:如果余数为 0。yeah!终于结束了,洗洗睡吧

当除到某一步时,余数等于 1… 停!stop!等一下,我发现有什么地方怪怪的。余数为 1,余数如果为 1 的话,再继续除下去,不就又是 1/7 了吗?绕了一个大弯,又回来了?对,你猜的很对,它永远不会结束,它循环了。

注意我上面说的 情况2,我们判断他循环,并 不是从直观看感觉它重复了,而是因为在计算过程中,它又回到了开头**。为什么这么说呢?当你计算一个分数时,它总是连续出现 5,出现了好多次,例如 0.5555555… 你也无法断定它是无限循环的,比如 一亿分之五。

记得高中时,从一本数学课外书学到了手动开平方的方法,于是很兴奋的去计算 2 的平方根,发现它的前几位是 1.414,哇,原来「2的平方根」等于 1.414141…。很多天以后,当我再次看到我的笔记时,只能苦笑了,「2的平方根」不可能循环啊,它可是一个无理数啊。

你可能不耐烦了,叽哩哇啦说这么多,有用吗?当然有用了,以后如果 MM 问你:你会爱我到什么时候?你可以回答她:我会爱你到 1/7 的尽头。难道我会把我的表白方式告诉你们吗? 我对你的爱就像圆周率,无限——却永不重复。

扯远了,现在会到主题。你也许会说:我明白了,循环小数不能精确表示,放到计算机中会丢失精度;那么有限小数可以精确表示吧,比如 0.1。

对于无限小数,不只是计算机不能精确表示,即使你用别的办法(省略号除外),比如纸、黑板、写字板…都无法精确表示。什么?手机?也不能,当然不能了。不,不,iPad也不行,1万买的也不行,真的,再贵的本子也写不下。

哪些数能精确表示?

那么 0.1 在计算机中可以精确表示吗?

答案是出人意料的, 不能。

在此之前,先思考个问题:在 0.1 到 0.9 的 9 个小数中,有多少可以用二进制精确表示呢?

我们按照乘以 2 取整数位的方法,把 0.1 表示为二进制(我假设那些不会进制转换的同学已经补习完了):(1) 0.1 x 2 = 0.2 取整数位 0 得 0.0(2) 0.2 x 2 = 0.4 取整数位 0 得 0.00(3) 0.4 x 2 = 0.8 取整数位 0 得 0.000(4) 0.8 x 2 = 1.6 取整数位 1 得 0.0001(5) 0.6 x 2 = 0.2 取整数位 1 得 0.00011(6) 0.2 x 2 = 0.4 取整数位 0 得 0.000110(7) 0.4 x 2 = 0.8 取整数位 0 得 0.0001100(8) 0.8 x 2 = 1.6 取整数位 1 得 0.00011001(9) 0.6 x 2 = 1.2 取整数位 1 得 0.000110011(n) ...

我们得到一个无限循环的二进制小数 0.000110011...

我为什么要把这个计算过程这么详细的写出来呢?就是为了让你看,多看几遍,再多看几遍,继续看…还没看出来,好吧,把眼睛揉一下,我提示你,把第一行去掉,从 (2) 开始看,看到 (6),对比一下 (2) 和 (6)。然后把前两行去掉,从 (3) 开始看…

明白了吧,0.2、0.4、0.6、0.8 都不能精确的表示为二进制小数。难以置信,这可是所有的偶数啊!那奇数呢?答案就是:

0.1 到 0.9 的 9 个小数中,只有 0.5 可以用二进制精确的表示。

如果把 0.0 再算上,那么就有两个数可以精确表示,一个奇数 0.5,一个偶数 0.0。为什么是两个呢?因为计算机二呗,其实计算机还真够二的。世界上有 10 种人,一种是懂二进制的,一种是不懂二进制的。

其实答案很显然,我再领大家换个角度思考,0.5 就是一半的意思。在十进制中,进制的基数是 10,而 5 正好是 10 的一半。2 的一半是多少?当然是 1 了。所以,十进制的 0.5 就是二进制的 0.1。如果我用八进制呢?不用计算你就应该立刻回答:0.4;转换成十六进制呢,当然就是 0.8 了。

(0.5)10 = (0.1)2 = (0.4)8 = (0.8)16

如果你还想继续思考,就又会发现一个有趣的事实,我们称之为 定理A。我们上面的数,都是小数点后面一位小数,因此,在十进制中,这样的小数有 10 个(就是 0 到 9);同理,在二进制中,如果我们让小数点后面有一位小数,应该有多少个呢?当然是 2 个了(0 和 1)。

哇,好像发现了新大陆一样,很兴奋是吧。那我再给你一棒,其实定理A是错的。再重申一遍 尽信书,则不如无书。我写博客的目的 不是把我的思想灌输到你的脑子里,你应该有自己的思想,自己的思考方式,当我得出这个结论时,你应该立刻反驳我:“按照你的思路,如果是 16 进制的话,应该可以精确表示所有的 0.1 到 0.9 的数甚至还可以精确表示其它的 6 个数。而事实呢,16 进制可以精确表示的数 和 2 进制可以精确表示的数是一样的,只能精确表示 0.5。”

那么到底怎么确定一个数能否精确表示呢?还是回到我们熟悉的十进制分数。

1/2、5/9、34/25 哪些可以写成有限小数?把一个分数化到最简(分子分母无公约数),如果分母的因式分解只有 2 和 5,那么就可以写成有限小数,否则就是无限循环小数。为什么是 2 和 5 呢?因为他们是 10 的因子 10 = 2 x 5。

二进制和十六进制呢?他们的因子只有 2,所以十六进制只是二进制的一种简写形式,它的精度和二进制一样。

如果一个十进制数可以用二进制精确表示,那么它的最后一位肯定是 5。

备注:这是个必要条件,而不是充分条件。一位热心网友设计出了下面的解决精度的方案。我就不解释了,同学们自己思考一下吧。我有一个观点,针对小数精度不够的问题(例如 0.1),软件可以人为的在数据最后一位补 5,也就是 0.15,这样牺牲一位,但是可以保证数据精度,还原再把那个尾巴 5 去掉。

请同学们思考一下。

精度在哪儿丢失?

一位热心网友 独孤小败 在 OSC 上回复了我上一篇文章,提出了一个疑问:

在 java 中计算 0.2 + 0.4 得到的结果是// 代码(a)double d = 0.2 + 0.4; // 结果是 0.6000000000000001

但是当直接输出 0.6 的时候,确实是 0.6// 代码(b)double d = 0.6; // 结果是 0.6

好像很矛盾。很显然,通过代码(b)可以知道,在 java 中,可以精确 显示 0.6,哪怕 0.6 不能被精确表示,但至少能精确把 0.6 显示出来,这不是和代码(a)矛盾了吗?

这又是一个 想当然的错误,在直观上认为 0.2 + 0.4 = 0.6 是必然成立的(在数学上确实如此),既然(a)的结果是 0.6,而且 java 可以精确输出 0.6,那么代码(a)的结果应该输出 0.6。

其实在计算机上 0.2 + 0.4 根本就不等于 0.6 (为什么?可以查看本系列『运算符』),因为 0.2 和 0.4 都不能被精确表示。浮点数的精度丢失在每一个表达式,而不仅仅是表达式的求值结果。

我们用数学中的概念类比一下,比如四舍五入,我们计算 1.6 + 2.8 保留整数。1.6 + 2.8 = 4.4

四舍五入得到 4。我们用另一种方法先把 1.6 四舍五入为 2再把 2.8 四舍五入为 3最后求和 2 + 3 = 5

通过两种运算,我们得到了两个结果 4 和 5。同理,在我们的浮点数运算中,参与运算的两个数 0.2 和 0.4 精度已经丢失了,所以他们求和的结果已经不是 0.6 了。

后记

上面一直在讨论小数,整数呢?在博客园,一位童鞋为下面的代码抓狂了:JSON.parse('{"status":1,"id":9986705337161735,"name":"test"}').id;

把这段代码复制到 Chrome 的 Console 中,按回车,诡异的问题出现了 9986705337161735 居然变成了 9986705337161736!原始数据加了 1。99867053371617359986705337161736

一开始以为是溢出,换了个更大的数:9986705337161738发现不会出现这个问题。

但是 9986705337161739 输出又变成了 9986705337161740!99867053371617399986705337161740

测试几次之后发现浏览器输出数字的一个规律(justjavac注:其实这个规律是错误的):十位数为偶数,个位数为奇数时会减 1,个位数为奇数时会加1

十位数为奇数,个位数为奇数时会加 1,个位数为奇数时会减1

又多测了几次,发现根本没有规律,很混乱!!有时候是加,有时候是减!!

解析:

这显然不仅仅是丢失精度的问题,欲知后事如何…咳咳…静待下一篇吧。

某网友回复

我觉得作者写的很好,不过有些问题说的不够清晰。这些误差都是在数制转换中产生的,而电脑在实际的浮点运算中并不存在数制转换,所以只要以16进制方式输出和输入就不存在误差。如果要以小数方式表示一个分数,任何数制都可能产生无限循环小数。二进制和十进制的区别在于底数为2而不是10,所以不能精确表示分母中含有5因子的分数,而十进制依然无法表示分母存在7,13,等等其他因子的最简分数。所以如果真的要避免无限循环小数,唯一的办法是使用分数表示法。而且实际上数制转换中产生的这些误差都非常微小。如您的例子 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1=0.8999999999999999实际上误差只有0.0000000000000001绝大多数场合这样的误差微不足道。9986705337161735这个数已经超过了32位整型数的上限,如果不出意料,最终是以浮点数的形式存在的,而把误差和第一个例子的0.0000000000000001进行对比,很容易明白这一切的原因:那就是这根本不是一个整数,而是一个浮点数,并且在数制转换中出现了误差。javascript作为一个弱类型的脚本语言虽然很不错,但是对于没有学习过经典的c,pascal等强制类型语言的新手来说,类型自动转换绝对是一个很棘手的问题。纵然js少有显式的类型转换,但是如果对类型转换如果没有深刻的了解显然会导致很多问题,作者文章中的许多问题就来源于此。(超出整型范围后的隐式转换,包括任何含有小数点的数,以及超过int32范围的数)事实上对于9986705337161735这种超长的id,如果加上引号,用字符串表示,或者进行一定的处理,转为数组储存,都是毫无问题的。计算机中对于精度要求很高的计算,必然需要运用到高精度库,浮点运算的作用原本就不在于精度。事实上浮点数被广泛使用的原因在于日常使用中,这种程度的精度已经足够了,比如圆周率通常运算中也只以3.1415926计算。

计算机无法准确计算浮点数,浮点数计算异常原因(转)相关推荐

  1. 用计算机浮点数表示法计算,计算机组成原理第4章浮点数运算方法ppt课件

    PPT内容 这是计算机组成原理第4章浮点数运算方法ppt课件下载,主要介绍了浮点数的表示:浮点加减运算:浮点加减运算的步骤:浮点数加减运算流程图:浮点加减法运算:浮点运算所需的硬件配置,欢迎点击下载. ...

  2. JS--使用第三方库解决浮点数(小数)计算不准确的问题

    原文网址:JS--使用第三方库解决浮点数(小数)计算不准确的问题_IT利刃出鞘的博客-CSDN博客 简介 本文介绍如何解决JavaScript在计算浮点数(小数)时不准确(精度丢失)的问题. 问题描述 ...

  3. Java中使用BigDecimal进行浮点数精确计算 超大整数 浮点数等计算,没有数位限制...

    (注:前面写了一个超大整数相加的类,参见: 超大整数相加,超过了long的范围,你要怎么做!,后来有朋友评论说BigDecimal可以完全实现我的这这个功能,刚开始的时候,我还不服气,据我所知那里有这 ...

  4. Java中使用BigDecimal进行浮点数精确计算 超大整数 浮点数等计算 没有数位限制

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! (注:前 ...

  5. 第二章 Java浮点数精确计算

    1.实际意义 在实际开发中,如果需要进行float或double的精确计算(尤其是财务计算),直接使用float或double是不行的(具体的例子看下边的代码的main方法的测试结果),需要使用Big ...

  6. 题目:任意给定一个浮点数,计算这个浮点数的立方根。(基于二分法和牛顿迭代法)(基于Java实现)

    题目:任意给定一个浮点数,计算这个浮点数的立方根.(基于二分法和牛顿迭代法)(基于Java实现) 首先,来分析一下这道题,其实在leetcode上做了求解根号3的题之后,对于这种求解立方根的题,基本上 ...

  7. 【汇编语言与计算机系统结构笔记03】浮点数的计算机表示,IEEE 754,舍入(rounding),C语言中的浮点数

    本次笔记内容: 04.浮点数的计算机表示 文章目录 IEEE的浮点数标准 IEEE的754标准 浮点数示例 计算机中浮点数二进制表示 浮点数的类型 规格化浮点数(Normalized) 规格化浮点数示 ...

  8. 计算机组成原理浮点规格化,规格化浮点数

    本词条缺少概述图,补充相关内容使词条更完整,还能快速升级,赶紧来编辑吧! 规格化浮点数又称格式化输出,是指把一个浮点数按指定的格式进行转换.通常在报表统计展示.数据计算存储时需要格式化,常用的格式化函 ...

  9. 计算机组成原理:定点数和浮点数、校验码

    1.定点数和浮点数 小数点在计算器中并没有专门的器件进行存放.根据小数点的位置是否固定,可将数字分为定点数和浮点数. 定点数 定点数是小数点位置固定的数,小数点的位置由事先约定.同一种定点数的机器码中 ...

  10. 线性代数应用于计算机科学例子,为什么计算机科学家们应该了解量子计算?(三):算法棱镜折射出的科学...

    译者按: 本文译自 Aram Harrow 的 Why now is the right time to study quantum computing(原文链接), 经 Aram 授权翻译. Ara ...

最新文章

  1. c语言pushback用法,C语言:【动态顺序表】动态顺序表的初始化、打印、尾插PushBack、尾删PopBack...
  2. php gd库截图,php使用gd库实现截屏的实例代码
  3. Java实现二叉树的构建与遍历
  4. 串行 spi Flash 跨页编程的注意点
  5. android circleimageview 导入到eclipse,android项目从Eclipse迁移到Android studio中常见问题解决方法.pdf...
  6. 2020-11-10(安卓如何传递数据)
  7. 收费最低的云存储_百度云:虚拟主机11元/6个月,入门级云服务器60元/年
  8. grep 匹配制表符 和 换行符
  9. Oracle几种查找和删除重复记录的方法总结
  10. matlab练习程序(RGB2HSL、HSL2RGB)
  11. StarCraft的工程师谈美国的游戏开发过程
  12. python学习笔记爬虫——爬取网络表情包
  13. css设置内外边距,css内外边距属性
  14. [C++]cpp小笔记3 --- C++ String and char
  15. python基础之温度转换
  16. 二叉平衡树的基本操作(完整代码)
  17. js pug 代码_jquery.form.js的使用(基于pug)
  18. 计算机文档里的圆圈,请问在WORD文档里怎样插入数字上的圆点?
  19. 讲道理 | 特征值和特征向量意义
  20. Centos7 安装DB2

热门文章

  1. Python 四舍六入五成双
  2. 西北大学第四届程序设计竞赛新生赛(同步赛)(J)
  3. gmsv源代码c语言,石器时代ABLua的原理、简介、过程、运用、收发
  4. 【模型】【课程笔记】01+02+03 金融风险管理导论
  5. 2022-04-28 Unity核心1——图片导入与图片设置
  6. 地理科学与计算机技术的关系,地理信息系统与地理学的关系
  7. 慧居科技在港交所上市状态“失效”,双良科技为控股股东
  8. 【爱情叙记】--刚闹完别扭
  9. CorelDRAW VBA - 第一个Hello World程序
  10. yxc_第一章 基础算法(三)_区间合并