日期:2021年11月29日标签:Developer

计算机是如何存储浮点数的—IEEE754 #

多年以前在大学学习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 #

首先看一下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.10.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);

(完)

目录