2. 精度损失(存时)
二进制浮点数的精度损失,你怎么看?
Last updated
二进制浮点数的精度损失,你怎么看?
Last updated
数字有精度损失——不能被精准表示,其实并不需要大惊小怪。
只是我们熟悉且默认了十进制世界里的“除不尽/无限小数”的表述方式,比如 1/3 = 0.333...3,π = 3.1415926535897931...,而到了二进制的世界里,第一反应却是“欸?”。
与其说我们是惊叹于“浮点数在内存中竟然会损失精度?!”,倒不如说是还不习惯二进制的世界。大约就是在说起“(十进制里的)这小数(到了二进制里)怎么就成无限的(不能被精准表示)了呢”时,会默认省掉括号里的进制数。
如果能稍微静下心来回忆下十进制里的“无限小数”,大概就能心平气和地接受二进制里的“不那么精准”了。
1/3
0.333...3
3 循环
1/6
0.1666...6
6 循环
1/7
0.142857...142857
142857 循环
1/9
0.111...1
1 循环
2/3
0.666...6
6 循环
2/7
0.2857142857...142857
142857 循环
2/9
0.222...2
2 循环
12/13
0.923076...923076
923076 循环
...
实数和数轴上的点是一一对应的,我们可以将其直观地看成是有限小数和无限小数的集合,但却无法用枚举的方式来描述实数这个整体。
实数分为有理数和无理数:有理数就是分数(整数即分母为 1 的分数),比如 0、1、0.5、1/5、1/3 等;不是有理数的实数都是无理数,无理数也称无限不循环小数,比如 π、欧拉数 e、黄金比例 φ、根号 2 等。
浮点数是有理数,因为它可以表示成两个整数相除的形式,比如 1.65 * 104 = (165 / 102) * 104,基数决定了分母的值。然而无论基数是多少,都无法避免有理数中出现“无限”循环小数的情况。比如 1/5 能用十进制精准表示,却不能被二进制浮点数精准表示:1/5 = 0.2(10) = 0.0011...0011(2)。1/3 既不能用十进制精准表示,也不能用二进制精准表示,但却能被三进制精准表示:1/3 = 0.33...33(10) = 0.01...01(2) = 0.1(3) 或 3-1。
目前,在计算机中表示“实数的近似值”的最常见的方式就是浮点表示。IEEE 754 标准提供了很多如何设计浮点数及其运算的规则,旨在既能为专家提供复杂的数值库,又能为程序员提供安全可靠的默认值。
当然,实数的表示也有其它替代方案。比如定点表示、对数系统、任意精度浮点运算、浮点扩展、Mathematica。比如将数字表示为具有整数分子和分母的分数形式,这样就能准确地表示任何有理数了(此时需要对单个整数使用 bignum 算法)。
下图是浮点数到实数的映射,中间有覆盖不到的小断层哦。
浮点数的范围是“线性地”取决于有效数的范围,“指数式”地取决于指数的范围,指数为数字附加了非常广的范围。
在日常算术中,我们常常需要保留数值的 n 位有效数字或者是保留小数点后的 n 位数字。比如,将以下数值保留 4 位有效数字:
有理数 1/3 = 0.33...33 ≈ 0.3333
圆周率 π = 3.141592657... ≈ 3.142
重力加速度 g = 9.80 m/s2 = 9.800 m/s2
珠峰高度 8848.86 米 ≈ 8849 米
真空中光速 299792458 m/s = 2.99792458 * 108 m/s ≈ 2.998 * 108 m/s
电子质量 9.10938215 * 10-31 kg ≈ 9.109 * 10-31 kg
不知大家是否有注意到,在我们给数值“保留 4 位有效数字”的时候,有的就已经在丢失精度了!
不同的是,这里我们是有意识地“主动”丢掉精度,而在浮点数的表示里,是计算机在帮我们“默默地”裁剪精度,根据其特定的有效位个数,比如 IEEE 754 binary64 就只能保留 53 个有效数字)。
在浮点数里,损失精度的情况也类似。情况包括但不限于:
无限循环的有理数,如 1/5 = 0.2(10) = 0.0011...0011(2)
无限不循环的无理数,如 π
四舍五入直接裁掉多余的
所以,就“损失精度”而言,二进制和十进制的本质是一样的,只是无限循环的有理数的集合变了而已。
之所以绕绕弯弯地用十进制来解释精度损失的情况,是因为它是浮点数表示和运算的基础。而且后面的什么上溢、下溢、大数危机、最大数和最大安全数等听起来比较唬人的概念,都和它有直接关系。
最重要的是,一旦我们用熟悉的十进制来理解这些,一切逻辑就都显得顺理成章自然而然了,理解起来会轻松很多。