计算机组成原理 | 为什么浮点数运算不精确?(阿里笔试)

简介: 计算机组成原理 | 为什么浮点数运算不精确?(阿里笔试)

前言


最近在公众号阿里技术上看到一套孤尽老师出的 10道Java测试题(据说阿里 P7 工程师的答题正确率只有 50%) ,其中有几道题是关于浮点数的,聪明的你,在评论区留下答案吧。


(1)
float a = 0.125f; 
double b = 0.125d;
System.out.println((a - b) == 0.0); 
代码的输出结果是什么?
A. true
B. false
(2)
double c = 0.8;
double d = 0.7;
double e = 0.6;
那么 c-d 与 d-e 是否相等?
A. true
B. false
(3)
System.out.println(1.0 / 0); 的结果是什么?
A. 抛出异常
B. Infinity
C. NaN
(4)
System.out.println(0.0 / 0.0); 的结果是什么?
A. 抛出异常
B. Infinity
C. NaN
D. 1.0
(5) 引用自《技术之瞳》
以下数字在表示为double(8字节的双精度浮点数)时存在舍入误差的有:
A 100 
B 根号2 
C 10^30
D 0.1 
E 0.5
(6) 
写出float x 与“零值”比较的if语句
复制代码


目录

image.png

1. 相关概念


关于浮点数的相关概念如下,在下面的分享中,我将不重复解释:


image.png


2. 计算机中数据的表示方法


  • 在你的Chrome浏览器上按F12,然后找到console,输入表达式0.1 + 0.2,回车
  • 在你的电子计算器上按0.1 + 0.2 =


你会发现前者的结果是0.30000000000000004,而后者的结果是0.3(当然了!)。那么,为什么计算机的准确度,连普通的电子计算器的都比不上?关键在于计算机与计算器使用了不同的数据表示方法

image.png


2.1 n 位二进制可以表示的信息量


对于整数来说,大家都知道8位有符号整数可以表示[-128,127],8位无符号整数可以表示[0,255],不管怎么样,8位二进制无论如何也只能表示256个整数。当需要表示257这个数,有且只有两个办法:


  • 1、增加位数,例如9位二进制可表示的数值范围就可以容纳257这个数
  • 2、改变编码规则,例如规定真值是在机器数的基础上加一,这样的话,0000,0000就表示数11111,1111就表示数257。(事实上,这就是移码干的事情,3.1节会再提到)


这就是计算机的自有属性,数字计算机只能处理离散数据,二进制的位数直接决定了它能表示的离散数据个数,也决定了它所能表示的信息个数,对于n位二进制数,它可以表示的信息量为2N2^N2N


同理,我们把问题域扩展到全体实数,8位二进制同样也只能表示256个实数。假如约定这样一种8位编码:最低两位为小数区域,其余是整数区域,这样就有:


000000.00 // 表示 0.0
000000.01 // 表示 0.25
000000.10 // 表示 0.5
000000.11 // 表示 0.75
000001.00 // 表示 1.0
000001.01 // 表示 1.25
... 此处省略250个数
复制代码


我们发现,介于0.0到0.25的数字被跳过了,而即使把小数区域的位长扩大到8位、16位、甚至一个极大的位数,也无法充分表示介于0.0到0.25所有的数。这是因为,在0.0到0.25之间的数是连续的,有无限多个数,但是有限的N位长二进制最多只能表示2N2^N2N个信息量,有限的信息量无法表示无限的数据量,这就是现实世界与计算机世界的矛盾。


2.2 定点数表示


实数有两种表示格式,分别是定点数浮点数。像上面说的这种约定整数部分和小数部分为固定位置的格式,就是定点数表示。


  • 定义:定点数(fixed point numbers) 约定机器数中的小数点总是固定在某个特定的位置。
  • 格式: 分为符号位、整数部分、隐含的小数点、小数部分。
  • 特点:整数部分和小数部分位长固定,当需要表示绝对值特大或者特小的数需要很大的空间


2.3 浮点数表示


我们已经知道32位二进制可以表示的信息量有232≈4∗1092^{32}\approx 4*10^92324109,但是很多语言都会宣称它们的32位单精度浮点数的数值范围约为−3.4∗1038~3.4∗1038-3.4*10^{38}~ 3.4*10^{38}3.410383.41038(左右边界),这是因为采用了浮点数格式。


  • 定义:浮点数(floating point numbers)使用科学计数法存储数字,小数点的位置根据指数的大小而浮动。
  • 格式: 分为符号位、指数、尾数 :

N=2E∗MN=2^E*MN=2EM

  • 特点: 一部分位作为指数,可以扩大所表示的数值范围
  • 意义: 是数字计算机表示实数的格式,并以IEEE 754 (IEEE Standard for Binary Floating-Point Arithmetic)为标准。


2.4 定点数和浮点数的区别


  • 表示范围:浮点数一部分位为指数,相同位长,浮点数格式所能表示的数值范围远远大于定点数格式
  • 精度大小:浮点数格式只有一部分位是有效数值位,相同位长,浮点格式的精度比定点格式低
  • 运算复杂度:浮点数主要包括指数和尾数两部分,运算时需要对阶、尾数计算、规格化等步骤,浮点运算比定点运算复杂
  • 溢出:定点运算在数超过可表示数值范围即发生溢出;在浮点运算中,只有规格化后数值超过指数所能表示的范围才溢出。


2.5 计算机表示实数的步骤


前面讲到相关概念时提到了实数的概念,具体如下:


image.png

一个虚数上相当于两个实数,所以我们只需要关心实数在计算机中的表示即可,将一个实数装载入计算机需要分为三个步骤:


2.5.1 转换为二进制数格式


这个步骤可能损失精度,换句话说,有些数会损失精度,而有些数不会,这取决于表示这个数需要的信息量和浮点数的存储格式


  • 无理数(无限不循环小数) 包含的信息量是无限的,例如圆周率π\piπ,没有任何一本书能够写到圆周率最后一位,java.lang.Math.PI也只是π\piπ的近似值,类似的,使用有限的二进制位自然无法精确表示;
  • 有限循环小数包含的信息量是有限的,它的信息量分为整数部分+小数不循环部分+小数循环部分,例如1.8333333...=1.83‾1.8333333... = 1.8\overline{3}1.8333333...=1.83。但是浮点数的表示方法分为符号位、指数区域和尾数区域,并不会单独用一块区域来存储循环的部分,因此有限循环小数也无法精确表示;
  • 最后剩下整数和有限小数,它们包含的信息量也是有限的,关键看是否有因子5。举两个例子:0.1和1万亿,请问哪个数能用二进制数精确表示?


从十进制看,0.1拥有2个信息量(个位数为0,第一位小数为1),1万亿拥有一万亿个信息量,二选一的话,肯定是选择信息量更低的0.1。但是,从二进制看,我们会发现0.1转换为二进制居然是一个无限循环小数0.00011‾0.0\overline{0011}0.00011(将整数部分除2取余、小数部分乘2取整来完成转换),所以答案是:1万亿可以精确表示,而0.1无法精确表示!


事实上,在0.1 到 0.9 的 9 个小数中,只有 0.5 可以用二进制精确的表示。怎么理解呢?我们把1想象成一个圆,在十进制里,它可以划分为10等分;但在二进制里,它只能划分为2等分。 也就是说二进制里一位,要么表示0,要么表示一半,它没有办法像十进制那样表示3/10、4/10、6/10...... 1的一半在十进制里是什么?0.5,所以二进制可以精确表示0.5,任何包含因子5的数都可以用二进制精确表示。无法精确表示的数字,存储值只能是真实值的近似表示。


提示: 类似地,思考下十进制数格式可以精确表示1/3吗?


2.5.2 转换为二进制科学计数法表示


这个步骤将二进制小数转换为规范化的科学计数法表示:N=a∗BEN = a * B^EN=aBE,因为只是写法的转换,所以这一步没有精度损失。


2.5.3 转换为IEEE 754 标准格式


IEEE 754严格规定了尾数域和指数域可表示的大小,位数有限,意味着信息量是有限的。有些数需要的二进制数据量巨大,在这个步骤自然会损失精度,具体如下:


  • 大于浮点数可以表示的最大绝对值:上溢(溢出到±∞\pm\infty±
  • 小于浮点数可以表示的最小绝对值:下溢(溢出到±0\pm0±0
  • 尾数有效位数超过尾数域位数(另外还有隐含的整数位1):舍入误差


3.  IEEE 754 标准的浮点数


IEEE 二进制浮点数算术标准(IEEE 754)是广泛使用的浮点数运算标准,是大多数高级语言的现行浮点运算标准,例如C/C++、Java、JavaScript等。


image.png

3.1 一般格式


浮点数格式的关键是科学计数法格式:N=a∗BEN = a * B^EN=aBE,其中:


  • a称为尾数(mantissa),或称有效数字(significand)
  • B称为基数(base),在二进制数中,基数是2
  • E称为指数(exponent)


一个数的科学计数法表示是不唯一的,举个例子,对于二进制数1111.0000(2)1111.0000_{(2)}1111.0000(2)来说,以下都是合法的科学计数法表示:111.1∗2111.1*2111.1211.11∗2211.11*2^211.112211110∗2−111110*2^{-1}1111021,但这些都不是规格化的表示,唯一规格化的表示为:1.111∗231.111*2^31.11123


对于一个科学计数法表示,当尾数a的整数部分有且仅有一位有效数字时,我们称它是规格化的。由于0在数字的最左边是无效的,而在二进制的世界里只有0和1,因此,二进制数使用规格化的科学计数法时,整数部分固定为1。


既然整数部分1是固定的,那么就没有必要存储整数部分的信息了。正因如此,IEEE 754 标准的浮点数采用隐藏位的策略,整数部分的1是隐含的,不需要占用一位比特,这样是使得尾数可以多一位有效数。


综上,IEEE 754 浮点数的一般格式如下:N=(−1)s∗1.f∗2EN = (-1)^s*1.f*2^EN=(1)s1.f2E


image.png


现在,我们已经知道浮点数划分的三个区域,现在我们来看这三个区域是如何求值的:


  • 符号位:0表示正,1表示负
  • 指数区域:移码指数区域采用移码表示:E=机器数−biasE = 机器数 - biasE=bias,偏移值bias=2位长−1−1bias=2^{位长-1}-1bias=211例如位长为8时,bias=127bias=127bias=127,位长为11时,bias=1023bias=1023bias=1023。注意:指数域全0和全1为特殊值
  • 尾数区域:隐藏整数位的原码尾数区域采用原码表示:1.f=1+机器数1.f = 1 + 机器数1.f=1+


举个例子,十进制数100(10)100_{(10)}100(10)转换为二进制为1.100100∗2(2)61.100100*2^6_{(2)}1.1001002(2)6。这里推荐一个站点:浮点数转换器,它可以很方便地对比实数的真值与机器数表示,如下图所示:


image.png

3.2 两种常用格式


前面讲的是IEEE 754 浮点数的一般格式,其中最常用的是32位单精度浮点数64位双精度浮点数,在高级语言中通常代表floatdouble两种数据类型(例如C/C++、Java),在有些语言中只有一种数字格式number(例如JavaScript/TypeScript)。


  • 单精度单精度浮点数有8位指数,23位尾数,再加上隐藏的整数1,总共有24位二进制精度
  • 双精度双精度浮点数有11位指数,52位尾数,再加上隐藏的整数1,总共有53位二进制精度,具体如下:

image.png


3.3 特殊值


在 IEEE 754 标准规定指数区域全0 或 全1为特殊值,具体如下:


  • 非规范化数(Denormalized Number)
  • 定义:指数域全0,尾数域不为0(去掉隐含整数域为1的约定)
  • 意义:可以保存绝对值更小的数,所有可表示的浮点数的差值都可以表示
  • +0/-0
  • 定义:指数域全0,尾数域全0(去掉隐含整数域为1的约定)。IEEE 754 未要求具体的尾数域,意味着NaN不是一个而是一族。
  • 意义:符号位为0是+0,符号位为1是-0,在涉及无穷的运算中避免丢失符号信息,例如11/x=x\frac{1}{1/x} = x1/x1=x,如果0不区分正负,在x=±∞x=\pm\inftyx=±时不成立
  • 正负无穷(Infinity)
  • 定义:指数域全1,尾数全0
  • 意义:用于表达计算中产生的上溢(overflow),使得计算中出现上溢不至于终止计算
  • 产生:除了NaN外的非零值除以0,其结果为正负无穷
  • NaN(Not a Number)
  • 定义:指数域全1,尾数域不为0
  • 意义:表示计算中的错误情况,例如0.00.0\frac{0.0}{0.0}0.00.02\sqrt22,使得计算中出现错误不至于终止计算
  • 特点:NaN是无序的,比较操作符在任一操作数为NaN是为false!=在任一操作数为NaN时为true,这意味着NaN != NaN


4. 总结


为什么 0.1 + 0.2 != 0.3 呢?首先,0.1 和 0.2 这两个实数无法用二进制精确表示。在二进制的世界里,只有包含因子 5 的十进制数才有可能精确表示,而 0.1 和 0.2 转换为二进制后是无限循环小数,在计算机中存储的值只能是真实值的近似表示,这里是第一次精度丢失;其次,计算机浮点数采用了 IEEE 754 标准格式,IEEE 754 严格规定了尾数域和指数域可表示的大小,位数有限,意味着可表示的信息量是有限的,换句话说就会存在三种误差:上溢、下溢和舍入误差。而 0.1 + 0.2 的结果的尾数域部分刚好超过了尾数域位数,超过位数的部分舍去,存在舍入误差,这里是第二次精度丢失。

目录
相关文章
|
7月前
|
算法 C语言 索引
计算机简单算法
计算机简单算法
55 1
|
7月前
|
算法 C语言
计算机简单算法举例
计算机简单算法举例
39 1
|
存储
计算机底层知识之处理小数
计算机精度缺失 推荐阅读指数 ⭐️⭐️⭐️ 如何用二进制表示小数 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️ 计算机精度缺失的原因 浮点数 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️ 正则表达式和EXCESS系统 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
153 0
计算机底层知识之处理小数
|
Python
一日一技:为什么浮点数在计算机中可能不准确?
一日一技:为什么浮点数在计算机中可能不准确?
89 0
408王道计算机组成原理强化——数据的运算及大题(上)
408王道计算机组成原理强化——数据的运算及大题
223 1
408王道计算机组成原理强化——数据的运算及大题(上)
408王道计算机组成原理强化——数据的运算及大题(下)
408王道计算机组成原理强化——数据的运算及大题
579 1
408王道计算机组成原理强化——数据的运算及大题(下)
408计算机组成原理学习笔记——浮点数的表示和运算
408计算机组成原理学习笔记——浮点数的表示和运算
1169 1
408计算机组成原理学习笔记——浮点数的表示和运算
|
存储
410计算机组成原理学习笔记——运算方法和运算电路(三)
410计算机组成原理学习笔记——运算方法和运算电路(三)
569 1
410计算机组成原理学习笔记——运算方法和运算电路(三)
计算机组成原理<三>——数据的表示和运算(上)
计算机组成原理<三>——数据的表示和运算(上)
计算机组成原理<三>——数据的表示和运算(上)
下一篇
DataWorks