****光棍节加长版****
如果我告诉你,中关村配置最高的电子计算机的计算精度还不如一个便利店卖的手持计算器,你一定会反驳我:「今天写博客之前又忘记吃药了吧」。
你可以用最主流的编程语言计算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。
9986705337161735
9986705337161736
一开始以为是溢出,换了个更大的数:9986705337161738 发现不会出现这个问题。
但是 9986705337161739 输出又变成了 9986705337161740!
9986705337161739
9986705337161740
测试几次之后发现浏览器输出数字的一个规律(justjavac注:其实这个规律是错误的):
- 十位数为偶数,个位数为奇数时会减 1,个位数为奇数时会加1
- 十位数为奇数,个位数为奇数时会加 1,个位数为奇数时会减1
又多测了几次,发现根本没有规律,很混乱!!有时候是加,有时候是减!!
解析:
这显然不仅仅是丢失精度的问题,欲知后事如何…咳咳…静待下一篇吧。
分享到:
相关推荐
本文将详细介绍如何使用S7-200SMART进行双精度浮点数到单精度浮点数的转换,并提供相应的库文件及使用说明。 1. **浮点数类型**: 浮点数在计算机中分为单精度浮点数(32位,IEEE 754标准)和双精度浮点数(64位,...
本代码将双精度浮点数转换为单精度浮点数,适合浮点数为正值的转换。 使用后将占用VD2810~VD2970字节,欢迎交流。 本代码的完成经历了一段时间的刻苦研究,无偿提供给真正需要的人,希望同行少走弯路。 代码允许复制...
浮点数(单精度浮点数,双精度浮点数) 浮点数是一种数字表示方法,用于近似表示任意实数。在计算机中,浮点数由一个整数或定点数(即尾数)乘以某个基数(通常是 2)的整数次幂得到。这种表示方法类似于基数为 10 ...
本篇文章主要探讨了如何在S7-200 PLC中处理双精度浮点数转换为单精度浮点数的问题,这对于管理智能电表、远传水表等远程抄表系统的数据一致性至关重要。双精度浮点数提供了更高的精度,但在处理能力有限的S7-200 PLC...
例如,单精度浮点数适用于内存和计算资源有限的环境,而双精度浮点数则提供更高的精度,适合科学计算和工程应用。 5. **兼容性和广泛使用** IEEE 754已成为国际标准ISO/IEC 60559,并被几乎所有的现代微处理器所...
2. 四舍五入:根据精度设定,进行四舍五入操作,避免因浮点数运算误差导致的显示问题。 3. 整数和小数分离:将浮点数的整数部分和小数部分分开,便于后续处理。 4. 转换格式:将整数部分和小数部分转换为字符串,...
代码之谜(五)讨论了浮点数的精度问题,谁偷了你的精度?该篇博客旨在让读者更好地理解代码中的浮点数。 8. JavaScript 语言 代码之谜系列博客大多使用 JavaScript 语言作为示例,讨论了 JavaScript 语言中的各种...
[C語言][教學]_基本語法#09._基本型態二_-_浮點數Float與雙精度浮點數Double
2. 双精度浮点数与PLC的数据处理能力:流量仪表等智能设备通常使用双精度浮点数存储数据,但由于S7-200 PLC只能处理单精度浮点数,因此无法直接使用这些双精度仪表数据,这限制了PLC进一步的数据处理及应用。...
浮点数在计算机科学中扮演着至关重要的角色,因为它们被用来表示小到极小、大到极大的数值,广泛应用于科学计算、图形处理、物理...通过实验,你可以亲手操作,直观感受浮点数的表示与运算过程,加深理论知识的理解。
电子-浮点数转换成字符串.c,单片机/嵌入式STM32-F0/F1/F2
在计算机中,实数通常以浮点数的形式存储,因为它们的精度和范围比定点数更大。浮点数是由一个符号位、指数部分和尾数部分组成的,遵循特定的浮点数表示标准,如IEEE 754。 S7-200SMART PLC支持实数和浮点数的运算...
根据给定的文件信息,本知识点讲解将围绕AB-Micro系列PLC中如何将双精度浮点数转换为整形数进行展开,以下是详细知识点: 1. PLC在工业中的应用 PLC(可编程逻辑控制器)是工业自动化控制的核心设备之一。在物联网...
在C语言中,浮点数的精度问题是一个常见的挑战,因为它涉及到如何在有限的位数内表示无限的实数。浮点数的存储基于IEEE 754标准,这导致了一些数字无法被精确表示,从而引发了精度问题。以下是处理浮点数精度问题的...
首先,浮点数在PLC编程中常用于处理需要较高精度的数值运算。在TIA博途V17中,我们通常使用S7-1500或S7-1200系列PLC,它们支持浮点数(Real)类型的变量。然而,在某些应用中,可能需要对浮点数进行格式化处理,比如...
浮点数转换器,可将浮点数、单精度 双精度的数值转换为16进制发送
c语言浮点数高精度加法计算
IEEE 754标准是浮点数运算的国际标准,它定义了浮点数的存储格式和运算规则,广泛应用于各种计算机系统中,包括CPU和浮点运算器。这一标准的引入极大地提高了科学计算的可移植性。 浮点数在IEEE 754标准下以三元组{...
在计算机科学中,浮点数是一种用于表示数值的近似方式,主要分为单精度浮点数和双精度浮点数。这些数据类型广泛应用于各种计算,特别是在需要处理大量精确度和范围的数学运算中,例如科学计算、图像处理和游戏开发。...
本文将深入探讨单精度和双精度浮点数与十六进制数之间的转换。 首先,我们来了解浮点数的基本概念。浮点数是一种可以表示小数的数值类型,分为单精度(Single-precision)和双精度(Double-precision)。单精度...