在一些编程语言(如Javascript)中,为什么浮点型数值运算时会产生误差?

如: 0.1 + 0.2 = 0.30000000000000004 0.15 + 0.15 = 0.25 + 0.05 = 0.3 0.7 + 0.1 = 0.79999999999 注: 原问题是关于 JavaScript 的,但是浮点误差并不是 JS 的特性,因此更新了问题。
关注者
130
被浏览
8768

6 个回答

不仅在 JavaScript 中存在这个「问题」,所有的支持二进制浮点数运算(绝大部分都是 IEEE 754[1] 的实现)的系统都存在这个现象。

其原因就是,在有限的存储空间下,绝大部分的十进制小数都不能用二进制浮点数来精确表示。例如,0.1 这个简单的十进制小数就不能用二进制浮点数来表示。

所谓「计算机浮点数」,其实就是二进制的「科学计数法」。在十进制中,科学计数法的形式是:

m\times10^n(n\in Z)
相应的,二进制的科学计数法就是:

m\times2^n(m, n \in Z)
而在有限的存储空间下,十进制小数 0.1 无论如何也不能用这种形式来表示,因此,计算机在存储它时,产生了精度丢失,所以就出现了问题中所描述的现象。

二进制浮点数具体的储存、运算细节,可以查阅现在应用最广的 IEEE 754。

[1] IEEE 754: zh.wikipedia.org/wiki/I
通常情况下,小数是用 浮点数 表示的:
计算机中的浮点数
浮点指的是带有小数的数值,浮点运算即是小数的四则运算,常用来测量电脑运算速度。大部份计算机采用二進制(b=2)的表示方法。(bit)是衡量浮点数所需存储空间的单位,通常为32位或64位,分别被叫作单精度双精度

比如单精度的浮点数,由32个bit位。按照IEEE 754 标准,32位中有
1位是符号位(sign)
8位是指数位(exponent)
23位是数值 (fraction)

如下图所示:
那么这个数的数值就是

(-1)^{sign} \times 2^{exponent_{2}} \times fraction_{2}

比如对于0.5,就可以表示成sign = 0, exponent = -1, fraction = 1


但实际上IEEE 754对表示方法还做了一些优化,

1.fraction必须是1-2之间的一个小数,这样fraction就可以表示24位而不是23位数
比如原数为0.5, 那么实际上会表示成1.0\times2^{-1}
2.exponent的实际值是exponent-offset,单精度的offset为127
比如-1会表示成126,因为126-offest(127) = -1

这样实际的计算公式是:

(-1)^{sign} \times 2^{exponent_{2} - 127_{10}} \times (1 + fraction_{2})
比如0.5的单精度表示为:
0 01111110 00000000000000000000000

其中0为符号位

01111110为指数位,十进制为126, 所以实际的exponent为126 - 127 = -1,

而 00000000000000000000000 为fraction,十进制为0,

所以0.5f = (-1)^{0} \times 2^{126 - 127} \times (1 + 0_{2})

这种表示方法带来的问题就是很多浮点数不能精确表示,比如0.1的浮点数表示为:
0 01111011 10011001100110011001101
实际上值为(-1)^{0} \times 2^{123- 127} \times (1 + 0.10011001100110011001101_{2})\approx 0.10000000149011612


那么为什么会有0.1 + 0.2 = 0.30000000000000004也就不难理解了,
由于给出的case都是双精度的,我介绍一个单精度的例子:
12.0f - 11.9f = 0.10000038f
其中12.0f的单精度表示为
0 10000010 10000000000000000000000
即12.0 = (-1)^{0} \times 2^{3} \times (1 + 0.1_{2})
而11.9f为
0 10000010 01111100110011001100110
即11.9 = (-1)^{0} \times 2^{3} \times (1 + 0.01111100110011001100110_{2})
如果直接想减可以得到
(-1)^{0} \times 2^{3} \times (1 + 0.1_{2}) -
(-1)^{0} \times 2^{3} \times (1 + 0.01111100110011001100110_{2}) =
(-1)^{0} \times 2^{3} \times (0.00000011001100110011010_{2})

(-1)^{0} \times 2^{3} \times (0.00000011001100110011010_{2}) 是不符合 IEEE 754 规范的,因为
0.00000011001100110011010_{2} 并不是一个1.x的表示方法,所以实际上结果会变成

(-1)^{0} \times 2^{-4} \times (1.1001100110011010_{2}) = 0.10000038
我们可以对比0.1f的实际值:
(-1)^{0} \times 2^{123- 127} \times (1 .10011001100110011001101_{2})\approx 0.10000000149011612