众所不周知,大部分编程语言都算不出0.1+0.2=0.3。比如Javascript一通计算下的结果就为:0.30000000000000004!cloud.tencent
然后打开windows上自带的计算器,输入“0.1+0.2”,然后拿着计算结果抡圆了准备打我的小脸。
自制
其实,计算器看似正确的结果,只是计算器把存在精度问题的最后几位在显示的时候舍入掉了而已。我们可以理解为这是一种“障眼法”。
要理解这个现象,得先搞明白计算机是怎么看待小数的。我们平时用的是十进制,但计算机只懂二进制,也就是那一堆0和1。把十进制小数转成二进制,用的是“乘2取整”法。以 0.1 为例:
看出来了吗?0.1 的二进制是无限循环的:0.00011001100110011……(0011 循环)。同样,0.2 的二进制也是无限循环:0.0011001100110011……(0011 循环)。看不出来也不要紧,我们只要记住:我们十进制世界里大部分稀松平常的小数,二进制都是无法精准表达的。0.1 和 0.2在二进制世界里,就是“无限循环小数”,也就如同十进制无法精确表示 1/3 一样。除了0.1和0.2以外,π、√2等数学上的无理数(无限不循环小数),二进制下更是超纲了,计算机同样也是无法精确表示的。我们现在主流的编程语言,通用的存储法基本都是“IEEE 754浮点数标准”,只要是使用了这个存储标准的语言,比如JavaScript、C/C++、Java、以及曾号称人人都应该掌握的Python等等,都是无法准确计算出“0.1+0.2=0.3”的。IEEE 754是由电气与电子工程师协会(IEEE)于1985年制定的浮点运算技术标准。在这部“数字宪法”制定之前,各大计算机厂家都有自己的浮点数存储法则,你不兼容我的,我不care你的,主打一个各论各的,造成了很多混乱。IEEE 754标准就规定了浮点数在计算机中的存储格式,常见的有32位单精度和64位双精度。每个数被拆分为符号位、指数位和尾数位三部分,尾数位长度是固定的(如双精度为52位),那么遇到无限循环或者无限不循环小数的时候,怎么办?那就是把尾数“一剪没”了。这样截断或舍入,就给0.1和0.2造成了存储误差。两个近似值相加,误差累积,就导致了0.1+0.2≠0.3的现象。你可能又得问:浮点数存储为啥非得把尾数截了?计算机这么牛,全存了不行吗?很多人以为计算机存储和计算都是绝对精确的,其实这是个误解。存整数确实可以——比如数字 42,转成二进制 101010,放进固定大小的空间里,只要空间够大(不溢出),拿出来还是 42,分毫不差。就像把鸡蛋放进仓库,拿出来还是那个鸡蛋。但问题是,数学世界里的数远不止如此。无限循环和无限不循环的数,远远多于整数。计算机的存储空间是有限的,无论用多少位,都装不下一个无限长的二进制串。所以,它只能截取前几十位,后面的直接扔掉。你可能又会想:那能不能把空间做得再大点,存更多位?从几十位变成几百位也行啊!对绝大多数工程、图形、科学计算来说,“IEEE 754浮点数存储法”所用的 64 位双精度(约 15-17 位有效数字)已经足够精确。对不同领域来说,计算精度要求是不同的,天文学家能算准光年就很牛了|网络如设计飞机、渲染电影、模拟天气,没人需要知道 π 的第10000位是什么(当然,π能不能算到最后一位,在数学上意义重大,可能意味着数学大厦的地基比如微积分、极限理论被动摇)。况且,无理数有无穷多个,再大的空间也装不完。所以计算机选择了一种“务实”的策略:用有限的存储,近似地表示所有的实数。专业的相关学科就叫做计算机数值分析,研究的就是“近似”的艺术。对于任何一个数值计算问题,数值分析都会追问:误差有多大?算法快不快?结果稳不稳?能不能算出来?只要这四个问题都能给出满意的答案,那就是合格的计算结果。回到 0.1 + 0.2。这不是程序的 bug,而是二进制与有限存储共同作用的必然结果。理解这个“误差”,就是数值分析教给我们的最重要一课:认识误差,理解误差,最终学会与误差共存,并控制它。参考资料:
[1]https://bbs.huaweicloud.com/blogs/287063
[2]https://www.puntoflotante.net/FLOATING-POINT-FORMAT-IEEE-754.htm[3]https://www.h-schmidt.net/FloatConverter/IEEE754.html
转载请联系:[email protected]
凡本公众号转载、引用的文章 、图片、音频、视频文件等资料的版权归版权所有人所有,因此产生相关后果,由版权所有人、原始发布者和内容提供者承担,如有侵权请联系删除。