多年以前在大学学习C++时,知道了IEEE 754是目前计算机最普遍的浮点数表示方式。但是由于工作上与之打交道甚少,经常忘记这一标准的具体细节,所以我写了这篇文章详细记录下来。
表示数字有很多种方式。例如,我们书写时最常用的方式——固定小数点位置,在几个数字中间放置一个小数点表示小数,如果没有小数点就表示整数。
还有一种方式,是科学计数法,它由基数部分和指数部分组成。例如用科学计数法表示十进制整数50
。
5 x (10 ^ 1)
0.5 x (10 ^ 2)
0.05 x (10 ^ 3)
...
123.45
可以表示成如下几种方式。
0.12345 x (10 ^ 3)
1.2345 x (10 ^ 2)
12.345 x (10 ^ 1)
123.45 x (10 ^ 0)
1234.5 x (10 ^ -1)
...
其中5 x (10 ^ 1)
和1.2345 x (10 ^ 2)
被称作标准科学计数法,其中左侧数字部分的小数点左侧只有一位非0数字。
如果用标准科学计数法表示,表示二进制数字,则指数的底数为2。
二进制10100.110
表示为1.0100110 × (2 ^ 4)
。
IEEE 754本质上就是二进制的标准科学计数法。
首先看一下IEEE 754的存储结构。
符号(Sign) | 指数(Exponent) | 尾数(Fraction) | |
---|---|---|---|
单精度(Single Precision) | 1 [31] | 8 [30-23] | 23 [22-00] |
双精度(Double Precision) | 1 [63] | 11 [62-52] | 52 [51-00] |
C++中单精度浮点数,用4个字节表示,一个字节等于8bit,所以总共有32bit。最左侧一位用来存储符号位,23-30位存储科学计数法的指数部分,0-23位存储科学计数法的非指数部分,这里叫做尾数部分。双精度用8个字节表示,最左侧位为符号位,52-62表示指数部分,0-51表示尾数部分。
假设符号位为正,指数部分存储的是70,尾数部分是1.1001(二进制),则表示的数值为1.1001 x (2 ^ 70)
。
下面依次解释符号、指数和尾数三个部分。
IEEE 754的符号位用来表示正数和负数,符号位为0表示正数,1表示负数,it's so easy。
单精度结构下,IEEE 754的指数部分有8位,8位可以表示的最大整数为255(2 ^ 8 - 1),因为指数部分既要表示正数,也要表示负数,所以需要有一个偏移值,对于单精度这个偏移值为127,所以指数部分存储的是200,表示的是(200 - 127),0表示(0 - 127),即-127。
双精度结构下,IEEE 754的指数部分有11位,此时计算偏移值为1023,所以0表示-1023。
采用标准的二进制科学计数法,那么尾数部分小数点前肯定是1(二进制只可能是1和0,因为是标准科学计数法,所以是1)。既然确定了小数点前是1,那么不需要再用单独的1个bit表示,所以尾数部分所有bit用来表示小数点右侧的数值。例如存储了100111
,那么结果是1.100111
,所以32位浮点数的Fraction有32位,但是它表示了24位数值加一个小数点。
以上,就是通过IEEE 754表示浮点数的过程,如果理解了IEEE 754,很容易能计算出单精度和双精度所能表示的数值范围。
0.1 + 0.2 != 0.3
? #在采用 IEEE754 存储浮点数的语言中,存在 0.1 + 0.2 = 0.30000000000000004
的现象。例如 JavaScript、C++等。导致 0.1 + 0.2
不等于 0.3
的原因,正是 IEEE 754 存储浮点数的方式。
0.1
和 0.2
的二进制是一个无限循环的二进制小数,无论是单精度还是双精度,在存储它们时都需要进行截断存取,所以导致 0.1
存储值实际并不是 0.1
,存在一个误差,同理存储的 0.2
也存在一个误差,所以计算结果并不是一定准确的。
所以无论任何时候,在判断两个浮点数数值之和是否等于某个数,一定要考虑一定的误差。
在 JavaScript 中,Number.EPSILON
可以用来表示这个误差,我们可以这样判断是否相等。
x = 0.2;
y = 0.3;
z = 0.1;
equal = (Math.abs(x - y + z) < Number.EPSILON);
(完)