1. 计算机算错了吗?¶
今天在 Twitter 上看到这样一条 tweet:
It’s a simple question:
9999999999999999.0 - 9999999999999998.0
Does your favorite language give the right answer? http://geocar.sdf1.org/numbers.html
当然是等于 1 嘛~
然而大多数编程语言都没有得出正确答案——它们得出的结果是 2.0
:
language | output |
---|---|
C: | main(){printf("%lf\n",(double)9999999999999999.0-9999999999999998.0);} 2.000000 |
Java: | public class Foo{public static void main(String args[]){System.out.println(9999999999999999.0-9999999999999998.0);}} 2.0 |
Python: | >>> 9999999999999999.0 - 9999999999999998.0 2.0 |
Javascript: | 9999999999999999.0 - 9999999999999998.0 2.0 |
GoLang: | var a = 9999999999999999.0; var b = 9999999999999998.0; fmt.Printf("%f\n", a-b) 2.000000 |
Ruby: | irb(main):001:0> 9999999999999999.0 - 9999999999999998.0 2.0 |
R: | > 9999999999999999.0-9999999999999998.0 [1] 2 |
WHY?
因为 IEEE 754
。
2. IEEE 754¶
The IEEE Standard for Floating-Point Arithmetic (IEEE 754) is a technical standard for floating-point arithmetic established in 1985 by the Institute of Electrical and Electronics Engineers (IEEE). via en.wikipedia.org/wiki/IEEE_754)
1985 年,电气电子工程师协会(IEEE)制定了浮点数算数标准——IEEE 754,为许多CPU与浮点运算器所采用。IEEE 754 规定了四种表示浮点数值的方式:单精度(32位)、双精度(64位)、延伸单精度(43位以上,很少使用)与延伸双精度(79位以上,通常以80位实现)。现代的编程语言通常采用双精度(64位)进行浮点数算数运算。
2.1 浮点数实现¶
浮点数在计算机内部以二进制数表示,其结构如下:
Value = sign × exponent × fraction
(值 = 符号位 * 指数偏移值 * 分数值
)
其中:
- 符号位:正为 0,负为 1。
- 指数偏移值:即浮点数表示法中指数域的编码值,等于指数的实际值加上某个固定的值,IEEE 754 标准规定该固定值为
2^(e - 1) - 1
,其中 e 为指数偏移值部分的长度。 - 编码范围为
0 <= exponent <= 2^e - 1
,实际取值范围为-2^(e - 1) - 2 <= exponent <= 2^(e - 1) - 1
。-2^(e - 1) - 1
和2^(e - 1)
被用作特殊处理,见下方「非规约形式的浮点数」和「特殊值」。 -
分数值:
-
规约形式的浮点数:如果浮点数中指数部分的编码值在
0 < exponent <= 2^{e}-2
之间,且在科学表示法的表示方式下,分数 (fraction) 部分最高有效位(即整数位)是 1,那么这个浮点数将被称为规约形式的浮点数。- 根据定义,规约形式的浮点数中,分数部分第一位必为整数 1,故此 1 可以省去,只留小数部分,由此可以多存一位有效数字。
- 非规约形式的浮点数:如果浮点数的指数部分的编码值是 0,分数部分非零,那么这个浮点数将被称为非规约形式的浮点数。一般是某个数字相当接近零时才会使用非规约型式来表示。 此时,0 表示
-2^(e - 1) - 2
而不是-2^(e - 1) - 1
,以此来解决突然式下溢出(abrupt underflow)问题。
在使用 32 位存储的浮点数中,符号位占 1 位,指数偏移值占 8 位,分数值占 23 位。
在使用 64 位存储的浮点数中,符号位占 1 位,指数偏移值占 11 位,分数值占 52 位。(如图所示:)
2.1.1 IEEE 754 规定了三个特殊值:¶
- 如果指数是 0 并且尾数的小数部分是 0,这个数是 ±0(和符号位相关)
- 如果指数 =
2^(e - 1)
并且尾数的小数部分是 0,这个数是 ±∞(同样和符号位相关) - 如果指数 =
2^(e - 1)
并且尾数的小数部分非 0,这个数表示为不是一个数(NaN)
2.2 双精度有多精确?¶
双精度的分数部分共有 52 位,加上规约形式前面省略的 1 位,因此共有 53 位二进制精度。
能表示的最大数值(在不考虑指数部分的情况下)为:2^53 - 1 = 9007199254740991
(十进制,共 16 位有效数字)。
超过精度的数值会被舍入,IEEE 754 默认的舍入规则为就近舍入,即舍入到最近的偶数——二进制下末位为 0 的数值。
2.3 双精度浮点数表示 9999999999999999.0
和 9999999999999998.0
¶
考察 9999999999999999
和 9999999999999998
的二进制形式如下:
>>> bin(9999999999999999), bin(9999999999999998)
<<< ('0b100011100001101111001001101111110000001111111111111111', # 二进制共 54 位
'0b100011100001101111001001101111110000001111111111111110') # 同上
二进制共 54 位,规约后省略首位,仍有 53 位
2.3.1 9999999999999999.0
:¶
1. 9999999999999999 用二进制表示为:
100011100001101111001001101111110000001111111111111111
└─────────────────────────┬──────────────────────────┘
54 bit
2. 9999999999999999.0 科学表示法表示为:
2^53 × 1.00011100001101111001001101111110000001111111111111111
└───────────────────────┬──────────────────────────┘│
52 bit 超出 1 bit,舍去最后一位 1 并进一位
3. 9999999999999999.0 用双精度浮点数表示为:
0100001101000001110000110111100100110111111000001000000000000000
│└────┬────┘└────────────────────────┬─────────────────────────┘
sign exp 52 bit
等于 10000000000000000,即 1e+16
2.3.2 9999999999999998.0
:¶
1. 9999999999999998 用二进制表示为:
100011100001101111001001101111110000001111111111111110
└─────────────────────────┬──────────────────────────┘
54 bit
2. 9999999999999998.0 科学表示法表示为:
2^53 × 1.00011100001101111001001101111110000001111111111111110
└───────────────────────┬──────────────────────────┘│
52 bit 超出 1 bit 舍入:舍去最后一位 0
3. 9999999999999998.0 用双精度浮点数表示为:
0100001101000001110000110111100100110111111000000111111111111111
│└────┬────┘└────────────────────────┬─────────────────────────┘
sign exp 52 bit
等于 9999999999999998,即 9.999999999999998e+15
2.4 双精度浮点数表示 9999999999999999.0 - 9999999999999998.0
¶
因此,在双精度浮点数表示下,
9999999999999999.0 - 9999999999999998.0
= 10000000000000000.0 - 9999999999999998.0
= 2.0
也就可以理解了。
最后¶
二进制可以精确地表示绝大部分常见的 10 进制整数,但无法精确地表示大部分常见的 10 进制小数(仅能精确表示二进分数 m/2^n
)。
比如 0.1 + 0.2 == 0.30000000000000004
也是困扰无数计算机新手的经典问题。
双精度浮点数表示 | 实际值 | |
---|---|---|
0.1 | 0x3FB999999999999A | 0.100000000000000005551115123126 |
0.2 | 0x3FC999999999999A | 0.200000000000000011102230246252 |
0.3 | 0x3FD3333333333333 | 0.299999999999999988897769753748 |
Well, nobody's perfect. - Some Like It Hot (1959)